Compare commits

...

2 Commits

6 changed files with 344 additions and 63 deletions

View File

@@ -1,3 +1,11 @@
## 0.1.80.5-dev - 2026-03-13
- dev: YouTube: yt-dlp ZIP-Installation von GitHub, kein yesno-Dialog
## 0.1.80.0-dev - 2026-03-13
- dev: YouTube-Plugin: yt-dlp Suche, Bug-Fix Any-Import
## 0.1.79.5-dev - 2026-03-11 ## 0.1.79.5-dev - 2026-03-11
- dev: Changelog-Hook auf prepare-commit-msg umgestellt - dev: Changelog-Hook auf prepare-commit-msg umgestellt

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.80.0-dev" provider-name="ViewIt"> <addon id="plugin.video.viewit" name="ViewIt" version="0.1.81.0-dev" provider-name="ViewIt">
<requires> <requires>
<import addon="xbmc.python" version="3.0.0" /> <import addon="xbmc.python" version="3.0.0" />
<import addon="script.module.requests" /> <import addon="script.module.requests" />

View File

@@ -227,14 +227,14 @@ def _trakt_save_token(token) -> None:
addon.setSetting("trakt_token_expires", str(token.expires_at)) addon.setSetting("trakt_token_expires", str(token.expires_at))
TRAKT_CLIENT_ID = "5f1a46be11faa2ef286d6a5d4fbdcdfe3b19c87d3799c11af8cf25dae5b802e9"
TRAKT_CLIENT_SECRET = "7b694c47c13565197c3549c7467e92999f36fb2d118f7c185736ec960af22405"
def _trakt_get_client(): def _trakt_get_client():
"""Erstellt einen TraktClient falls client_id und client_secret konfiguriert sind.""" """Erstellt einen TraktClient mit den fest hinterlegten Credentials."""
client_id = _get_setting_string("trakt_client_id").strip()
client_secret = _get_setting_string("trakt_client_secret").strip()
if not client_id or not client_secret:
return None
from core.trakt import TraktClient from core.trakt import TraktClient
return TraktClient(client_id, client_secret, log=lambda m: _log(m, xbmc.LOGDEBUG)) return TraktClient(TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET, log=lambda m: _log(m, xbmc.LOGDEBUG))
def _trakt_get_valid_token() -> str: def _trakt_get_valid_token() -> str:
@@ -1167,6 +1167,7 @@ def _add_directory_item(
info_labels: dict[str, str] | None = None, info_labels: dict[str, str] | None = None,
art: dict[str, str] | None = None, art: dict[str, str] | None = None,
cast: list[TmdbCastMember] | None = None, cast: list[TmdbCastMember] | None = None,
context_menu: list[tuple[str, str]] | None = None,
) -> None: ) -> None:
"""Fuegt einen Eintrag (Folder oder Playable) in die Kodi-Liste ein.""" """Fuegt einen Eintrag (Folder oder Playable) in die Kodi-Liste ein."""
query: dict[str, str] = {"action": action} query: dict[str, str] = {"action": action}
@@ -1187,6 +1188,11 @@ def _add_directory_item(
setter(art) setter(art)
except Exception: except Exception:
pass pass
if context_menu:
try:
item.addContextMenuItems(context_menu)
except Exception:
pass
xbmcplugin.addDirectoryItem(handle=handle, url=url, listitem=item, isFolder=is_folder) xbmcplugin.addDirectoryItem(handle=handle, url=url, listitem=item, isFolder=is_folder)
@@ -1698,13 +1704,70 @@ def _sync_ytdlp_status_setting() -> None:
_set_setting_string("ytdlp_status", status) _set_setting_string("ytdlp_status", status)
def _fetch_ytdlp_zip_url() -> str:
"""Ermittelt die aktuellste ZIP-URL fuer script.module.yt-dlp von GitHub."""
api_url = "https://api.github.com/repos/lekma/script.module.yt-dlp/releases/latest"
try:
data = json.loads(_read_binary_url(api_url, timeout=10).decode("utf-8"))
for asset in data.get("assets", []):
url = asset.get("browser_download_url", "")
if url.endswith(".zip"):
return url
except Exception as exc:
_log(f"yt-dlp Release-URL nicht ermittelbar: {exc}", xbmc.LOGWARNING)
return ""
def _install_ytdlp_from_zip(zip_url: str) -> bool:
"""Laedt script.module.yt-dlp ZIP von GitHub und entpackt es in den Addons-Ordner."""
try:
zip_bytes = _read_binary_url(zip_url, timeout=60)
except Exception as exc:
_log(f"yt-dlp ZIP-Download fehlgeschlagen: {exc}", xbmc.LOGWARNING)
return False
if xbmcvfs is None:
return False
addons_root = xbmcvfs.translatePath("special://home/addons")
addons_root_real = os.path.realpath(addons_root)
try:
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
for member in archive.infolist():
name = str(member.filename or "")
if not name or name.endswith("/"):
continue
target = os.path.realpath(os.path.join(addons_root, name))
if not target.startswith(addons_root_real + os.sep):
continue
os.makedirs(os.path.dirname(target), exist_ok=True)
with archive.open(member, "r") as src, open(target, "wb") as dst:
dst.write(src.read())
except Exception as exc:
_log(f"yt-dlp Entpacken fehlgeschlagen: {exc}", xbmc.LOGWARNING)
return False
builtin = getattr(xbmc, "executebuiltin", None)
if callable(builtin):
builtin("UpdateLocalAddons")
return _is_addon_installed(YTDLP_ADDON_ID)
def _ensure_ytdlp_installed(*, force: bool, silent: bool) -> bool: def _ensure_ytdlp_installed(*, force: bool, silent: bool) -> bool:
if _is_addon_installed(YTDLP_ADDON_ID): if _is_addon_installed(YTDLP_ADDON_ID):
_sync_ytdlp_status_setting() _sync_ytdlp_status_setting()
return True return True
wait_seconds = 20 if force else 0 zip_url = _fetch_ytdlp_zip_url()
ok = _install_kodi_addon(YTDLP_ADDON_ID, wait_seconds=wait_seconds) if not zip_url:
_sync_ytdlp_status_setting()
if not silent:
xbmcgui.Dialog().notification(
"yt-dlp",
"Aktuelle Version nicht ermittelbar (GitHub nicht erreichbar?).",
xbmcgui.NOTIFICATION_ERROR,
5000,
)
return False
ok = _install_ytdlp_from_zip(zip_url)
_sync_ytdlp_status_setting() _sync_ytdlp_status_setting()
if not silent: if not silent:
@@ -1718,7 +1781,7 @@ def _ensure_ytdlp_installed(*, force: bool, silent: bool) -> bool:
else: else:
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification(
"yt-dlp", "yt-dlp",
"Installation fehlgeschlagen. Bitte Repository/Netzwerk pruefen.", "Installation fehlgeschlagen.",
xbmcgui.NOTIFICATION_ERROR, xbmcgui.NOTIFICATION_ERROR,
5000, 5000,
) )
@@ -3958,21 +4021,27 @@ def _trakt_scrobble_start_async(media: dict[str, object]) -> None:
from core.trakt import TraktClient from core.trakt import TraktClient
except Exception: except Exception:
return return
client_id = _get_setting_string("trakt_client_id").strip()
client_secret = _get_setting_string("trakt_client_secret").strip()
access_token = _get_setting_string("trakt_access_token").strip() access_token = _get_setting_string("trakt_access_token").strip()
if not client_id or not client_secret or not access_token: if not access_token:
return return
client = TraktClient(client_id, client_secret, log=lambda m: _log(m, xbmc.LOGDEBUG)) client = TraktClient(TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET, log=lambda m: _log(m, xbmc.LOGDEBUG))
media_type = str(media.get("kind", "movie"))
tmdb_id = int(media.get("tmdb_id", 0))
imdb_id = str(media.get("imdb_id", ""))
client.scrobble_start( client.scrobble_start(
access_token, access_token,
media_type=str(media.get("kind", "movie")), media_type=media_type,
title=str(media.get("title", "")), title=str(media.get("title", "")),
tmdb_id=int(media.get("tmdb_id", 0)), tmdb_id=tmdb_id,
imdb_id=str(media.get("imdb_id", "")), imdb_id=imdb_id,
season=int(media.get("season", 0)), season=int(media.get("season", 0)),
episode=int(media.get("episode", 0)), episode=int(media.get("episode", 0)),
) )
if _get_setting_bool("trakt_auto_watchlist", default=False) and (tmdb_id or imdb_id):
try:
client.add_to_watchlist(access_token, media_type=media_type, tmdb_id=tmdb_id, imdb_id=imdb_id)
except Exception:
pass
threading.Thread(target=_do, daemon=True).start() threading.Thread(target=_do, daemon=True).start()
@@ -3983,12 +4052,10 @@ def _trakt_scrobble_stop_async(media: dict[str, object], progress: float = 100.0
from core.trakt import TraktClient from core.trakt import TraktClient
except Exception: except Exception:
return return
client_id = _get_setting_string("trakt_client_id").strip()
client_secret = _get_setting_string("trakt_client_secret").strip()
access_token = _get_setting_string("trakt_access_token").strip() access_token = _get_setting_string("trakt_access_token").strip()
if not client_id or not client_secret or not access_token: if not access_token:
return return
client = TraktClient(client_id, client_secret, log=lambda m: _log(m, xbmc.LOGDEBUG)) client = TraktClient(TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET, log=lambda m: _log(m, xbmc.LOGDEBUG))
client.scrobble_stop( client.scrobble_stop(
access_token, access_token,
media_type=str(media.get("kind", "movie")), media_type=str(media.get("kind", "movie")),
@@ -4595,13 +4662,11 @@ def _trakt_authorize() -> None:
time.sleep(code.interval) time.sleep(code.interval)
from core.trakt import TraktClient from core.trakt import TraktClient
# Einzelversuch (kein internes Polling wir steuern die Schleife selbst) # Einzelversuch (kein internes Polling wir steuern die Schleife selbst)
client_id = _get_setting_string("trakt_client_id").strip() tmp_client = TraktClient(TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET, log=lambda m: _log(m, xbmc.LOGDEBUG))
client_secret = _get_setting_string("trakt_client_secret").strip()
tmp_client = TraktClient(client_id, client_secret, log=lambda m: _log(m, xbmc.LOGDEBUG))
status, payload = tmp_client._post("/oauth/device/token", { status, payload = tmp_client._post("/oauth/device/token", {
"code": code.device_code, "code": code.device_code,
"client_id": client_id, "client_id": TRAKT_CLIENT_ID,
"client_secret": client_secret, "client_secret": TRAKT_CLIENT_SECRET,
}) })
if status == 200 and isinstance(payload, dict): if status == 200 and isinstance(payload, dict):
from core.trakt import TraktToken from core.trakt import TraktToken
@@ -4718,6 +4783,15 @@ def _show_trakt_history(page: int = 1) -> None:
info_labels["plot"] = item.episode_overview info_labels["plot"] = item.episode_overview
info_labels["mediatype"] = "episode" if is_episode else "tvshow" info_labels["mediatype"] = "episode" if is_episode else "tvshow"
# Kontextmenue: Zur Watchlist hinzufuegen
ctx: list[tuple[str, str]] = []
if item.ids and (item.ids.tmdb or item.ids.imdb):
wl_type = "movie" if item.media_type == "movie" else "tv"
wl_params = urlencode({"action": "trakt_watchlist_add", "type": wl_type,
"tmdb_id": str(item.ids.tmdb), "imdb_id": item.ids.imdb})
ctx.append(("Zur Trakt-Watchlist hinzufuegen",
f"RunPlugin({sys.argv[0]}?{wl_params})"))
# Navigation: Episoden direkt abspielen, Serien zur Staffelauswahl # Navigation: Episoden direkt abspielen, Serien zur Staffelauswahl
match = _trakt_find_in_plugins(item.title) match = _trakt_find_in_plugins(item.title)
if match: if match:
@@ -4730,13 +4804,13 @@ def _show_trakt_history(page: int = 1) -> None:
"season": f"Staffel {item.season}", "season": f"Staffel {item.season}",
"episode": f"Episode {item.episode}", "episode": f"Episode {item.episode}",
} }
_add_directory_item(handle, label, action, params, is_folder=False, info_labels=info_labels, art=art) _add_directory_item(handle, label, action, params, is_folder=False, info_labels=info_labels, art=art, context_menu=ctx or None)
else: else:
action = "seasons" action = "seasons"
params = {"plugin": plugin_name, "title": matched_title} params = {"plugin": plugin_name, "title": matched_title}
_add_directory_item(handle, label, action, params, is_folder=True, info_labels=info_labels, art=art) _add_directory_item(handle, label, action, params, is_folder=True, info_labels=info_labels, art=art, context_menu=ctx or None)
else: else:
_add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True, info_labels=info_labels, art=art) _add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True, info_labels=info_labels, art=art, context_menu=ctx or None)
if len(items) >= LIST_PAGE_SIZE: if len(items) >= LIST_PAGE_SIZE:
_add_directory_item(handle, "Naechste Seite >>", "trakt_history", {"page": str(page + 1)}, is_folder=True) _add_directory_item(handle, "Naechste Seite >>", "trakt_history", {"page": str(page + 1)}, is_folder=True)
@@ -4756,7 +4830,7 @@ def _show_trakt_upcoming() -> None:
return return
xbmcplugin.setPluginCategory(handle, "Trakt: Upcoming") xbmcplugin.setPluginCategory(handle, "Trakt: Upcoming")
_set_content(handle, "episodes") _set_content(handle, "tvshows")
try: try:
from core.trakt import TraktCalendarItem as _TCI # noqa: F401 from core.trakt import TraktCalendarItem as _TCI # noqa: F401
@@ -4772,20 +4846,50 @@ def _show_trakt_upcoming() -> None:
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
from datetime import datetime, date as _date
_WEEKDAYS = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
today = _date.today()
# Datum pro Item berechnen und nach Datum gruppieren
dated_items: list[tuple[_date, object]] = []
for item in items: for item in items:
# Datum aufbereiten: ISO -> lesbares Datum airdate = today
airdate = ""
if item.first_aired: if item.first_aired:
try: try:
from datetime import datetime, timezone
dt = datetime.fromisoformat(item.first_aired.replace("Z", "+00:00")) dt = datetime.fromisoformat(item.first_aired.replace("Z", "+00:00"))
airdate = dt.astimezone(tz=None).strftime("%d.%m.%Y") airdate = dt.astimezone(tz=None).date()
except Exception: except Exception:
airdate = item.first_aired[:10] pass
dated_items.append((airdate, item))
last_date: _date | None = None
for airdate, item in dated_items:
# Datums-Ueberschrift einfuegen
if airdate != last_date:
last_date = airdate
delta = (airdate - today).days
if delta == 0:
heading = "Heute"
elif delta == 1:
heading = "Morgen"
elif 2 <= delta <= 6:
heading = _WEEKDAYS[airdate.weekday()]
else:
heading = f"{_WEEKDAYS[airdate.weekday()]} {airdate.strftime('%d.%m.')}"
sep = xbmcgui.ListItem(label=f"[B]{heading}[/B]")
sep.setProperty("IsPlayable", "false")
try:
_apply_video_info(sep, {"title": heading, "mediatype": "video"}, None)
except Exception:
pass
xbmcplugin.addDirectoryItem(handle=handle, url="", listitem=sep, isFolder=False)
# Episoden-Label mit Titel
ep_title = item.episode_title or ""
label = f"{item.show_title} \u2013 S{item.season:02d}E{item.episode:02d}" label = f"{item.show_title} \u2013 S{item.season:02d}E{item.episode:02d}"
if airdate: if ep_title:
label = f"{label} ({airdate})" label = f"{label}: {ep_title}"
info_labels: dict[str, object] = { info_labels: dict[str, object] = {
"title": label, "title": label,
@@ -4798,15 +4902,18 @@ def _show_trakt_upcoming() -> None:
info_labels["year"] = item.show_year info_labels["year"] = item.show_year
if item.episode_overview: if item.episode_overview:
info_labels["plot"] = item.episode_overview info_labels["plot"] = item.episode_overview
if ep_title:
info_labels["tagline"] = ep_title
# Artwork: Trakt-Bilder als Basis, TMDB ergänzt fehlende Keys # Artwork: Trakt-Bilder als Basis, TMDB ergaenzt fehlende Keys
art: dict[str, str] = {} art: dict[str, str] = {}
if item.episode_thumb:
art["thumb"] = item.episode_thumb
if item.show_fanart:
art["fanart"] = item.show_fanart
if item.show_poster: if item.show_poster:
art["thumb"] = item.show_poster
art["poster"] = item.show_poster art["poster"] = item.show_poster
if item.episode_thumb:
art["fanart"] = item.episode_thumb
elif item.show_fanart:
art["fanart"] = item.show_fanart
_, tmdb_art, _ = _tmdb_labels_and_art(item.show_title) _, tmdb_art, _ = _tmdb_labels_and_art(item.show_title)
for _k, _v in tmdb_art.items(): for _k, _v in tmdb_art.items():
art.setdefault(_k, _v) art.setdefault(_k, _v)

View File

@@ -18,7 +18,14 @@ except ImportError:
requests = None # type: ignore requests = None # type: ignore
from plugin_interface import BasisPlugin from plugin_interface import BasisPlugin
from plugin_helpers import log_error
try:
import xbmc # type: ignore
def _log(msg: str) -> None:
xbmc.log(f"[ViewIt][YouTube] {msg}", xbmc.LOGWARNING)
except ImportError:
def _log(msg: str) -> None:
pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Konstanten # Konstanten
@@ -121,13 +128,15 @@ def _videos_from_search_data(data: dict) -> List[str]:
if title and video_id: if title and video_id:
results.append(_encode(title, video_id)) results.append(_encode(title, video_id))
except Exception as exc: except Exception as exc:
log_error(f"[YouTube] _videos_from_search_data Fehler: {exc}") _log(f"[YouTube] _videos_from_search_data Fehler: {exc}")
return results return results
def _search_with_ytdlp(query: str, count: int = 20) -> List[str]: def _search_with_ytdlp(query: str, count: int = 20) -> List[str]:
"""Sucht YouTube-Videos via yt-dlp ytsearch-Extraktor.""" """Sucht YouTube-Videos via yt-dlp ytsearch-Extraktor."""
if not _ensure_ytdlp_in_path():
return []
try: try:
from yt_dlp import YoutubeDL # type: ignore from yt_dlp import YoutubeDL # type: ignore
except ImportError: except ImportError:
@@ -144,7 +153,7 @@ def _search_with_ytdlp(query: str, count: int = 20) -> List[str]:
if e.get("id") and e.get("title") if e.get("id") and e.get("title")
] ]
except Exception as exc: except Exception as exc:
log_error(f"[YouTube] yt-dlp Suche Fehler: {exc}") _log(f"[YouTube] yt-dlp Suche Fehler: {exc}")
return [] return []
@@ -161,31 +170,79 @@ def _fetch_search_videos(url: str) -> List[str]:
return [] return []
return _videos_from_search_data(data) return _videos_from_search_data(data)
except Exception as exc: except Exception as exc:
log_error(f"[YouTube] _fetch_search_videos ({url}): {exc}") _log(f"[YouTube] _fetch_search_videos ({url}): {exc}")
return [] return []
def _fix_strptime() -> None:
"""Kodi-Workaround: datetime.strptime ist manchmal None."""
import datetime as _dt
import time as _time
if not callable(getattr(_dt.datetime, "strptime", None)):
_dt.datetime.strptime = lambda s, f: _dt.datetime(*(_time.strptime(s, f)[0:6]))
def _ensure_ytdlp_in_path() -> bool:
"""Fuegt script.module.yt-dlp/lib zum sys.path hinzu falls noetig."""
_fix_strptime()
try:
import yt_dlp # type: ignore # noqa: F401
return True
except ImportError:
pass
try:
import sys, os
import xbmcvfs # type: ignore
lib_path = xbmcvfs.translatePath("special://home/addons/script.module.yt-dlp/lib")
if lib_path and os.path.isdir(lib_path) and lib_path not in sys.path:
sys.path.insert(0, lib_path)
import yt_dlp # type: ignore # noqa: F401
return True
except Exception:
pass
return False
def _get_quality_format() -> str:
"""Liest YouTube-Qualitaet aus den Addon-Einstellungen."""
_QUALITY_MAP = {
"0": "best[ext=mp4]/best",
"1": "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best",
"2": "bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]/best",
"3": "bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]/best",
"4": "bestvideo[height<=360][ext=mp4]+bestaudio[ext=m4a]/best[height<=360][ext=mp4]/best",
}
try:
import xbmcaddon # type: ignore
val = xbmcaddon.Addon().getSetting("youtube_quality") or "0"
return _QUALITY_MAP.get(val, _QUALITY_MAP["0"])
except Exception:
return _QUALITY_MAP["0"]
def _resolve_with_ytdlp(video_id: str) -> Optional[str]: def _resolve_with_ytdlp(video_id: str) -> Optional[str]:
"""Loest Video-ID via yt-dlp zu direkter Stream-URL auf.""" """Loest Video-ID via yt-dlp zu direkter Stream-URL auf."""
try: if not _ensure_ytdlp_in_path():
from yt_dlp import YoutubeDL # type: ignore _log("[YouTube] yt-dlp nicht verfuegbar (script.module.yt-dlp fehlt)")
except ImportError:
log_error("[YouTube] yt-dlp nicht verfuegbar (script.module.yt-dlp fehlt)")
try: try:
import xbmc
import xbmcgui import xbmcgui
yes = xbmcgui.Dialog().yesno( xbmcgui.Dialog().notification(
"yt-dlp fehlt", "yt-dlp fehlt",
"script.module.yt-dlp ist nicht installiert.\nJetzt installieren?", "Bitte yt-dlp in den ViewIT-Einstellungen installieren.",
xbmcgui.NOTIFICATION_ERROR,
5000,
) )
if yes:
xbmc.executebuiltin("RunPlugin(plugin://plugin.video.viewit/?action=install_ytdlp)")
except Exception: except Exception:
pass pass
return None return None
try:
from yt_dlp import YoutubeDL # type: ignore
except ImportError:
return None
url = f"https://www.youtube.com/watch?v={video_id}" url = f"https://www.youtube.com/watch?v={video_id}"
fmt = _get_quality_format()
ydl_opts: Dict[str, Any] = { ydl_opts: Dict[str, Any] = {
"format": "best[ext=mp4]/best", "format": fmt,
"quiet": True, "quiet": True,
"no_warnings": True, "no_warnings": True,
"extract_flat": False, "extract_flat": False,
@@ -204,7 +261,7 @@ def _resolve_with_ytdlp(video_id: str) -> Optional[str]:
if formats: if formats:
return formats[-1].get("url") return formats[-1].get("url")
except Exception as exc: except Exception as exc:
log_error(f"[YouTube] yt-dlp Fehler fuer {video_id}: {exc}") _log(f"[YouTube] yt-dlp Fehler fuer {video_id}: {exc}")
return None return None
@@ -215,8 +272,7 @@ def _resolve_with_ytdlp(video_id: str) -> Optional[str]:
class YoutubePlugin(BasisPlugin): class YoutubePlugin(BasisPlugin):
name = "YouTube" name = "YouTube"
# Pseudo-Staffeln: nur Suche Browse-Endpunkte erfordern Login _SEASONS = ["Stream"]
_SEASONS = ["Suche"]
def capabilities(self) -> Set[str]: def capabilities(self) -> Set[str]:
return set() return set()
@@ -242,8 +298,7 @@ class YoutubePlugin(BasisPlugin):
return list(self._SEASONS) return list(self._SEASONS)
def episodes_for(self, title: str, season: str) -> List[str]: def episodes_for(self, title: str, season: str) -> List[str]:
if season == "Suche": if season == "Stream":
# Titel ist bereits ein kodierter Eintrag aus der Suche
return [title] return [title]
return [] return []

View File

@@ -78,10 +78,9 @@
<category label="Trakt"> <category label="Trakt">
<setting id="trakt_enabled" type="bool" label="Trakt aktivieren" default="false" /> <setting id="trakt_enabled" type="bool" label="Trakt aktivieren" default="false" />
<setting id="trakt_client_id" type="text" label="Trakt Client ID" default="" />
<setting id="trakt_client_secret" type="text" label="Trakt Client Secret" default="" />
<setting id="trakt_auth" type="action" label="Trakt autorisieren" action="RunPlugin(plugin://plugin.video.viewit/?action=trakt_auth)" option="close" /> <setting id="trakt_auth" type="action" label="Trakt autorisieren" action="RunPlugin(plugin://plugin.video.viewit/?action=trakt_auth)" option="close" />
<setting id="trakt_scrobble" type="bool" label="Scrobbling aktivieren" default="true" /> <setting id="trakt_scrobble" type="bool" label="Scrobbling aktivieren" default="true" />
<setting id="trakt_auto_watchlist" type="bool" label="Geschaute Serien automatisch zur Watchlist hinzufuegen" default="false" />
<setting id="trakt_access_token" type="text" label="" default="" visible="false" /> <setting id="trakt_access_token" type="text" label="" default="" visible="false" />
<setting id="trakt_refresh_token" type="text" label="" default="" visible="false" /> <setting id="trakt_refresh_token" type="text" label="" default="" visible="false" />
<setting id="trakt_token_expires" type="text" label="" default="0" visible="false" /> <setting id="trakt_token_expires" type="text" label="" default="0" visible="false" />
@@ -124,6 +123,7 @@
<setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" /> <setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" />
</category> </category>
<category label="YouTube"> <category label="YouTube">
<setting id="youtube_quality" type="enum" label="YouTube Videoqualitaet" default="0" values="Beste|1080p|720p|480p|360p" />
<setting id="install_ytdlp" type="action" label="yt-dlp installieren/reparieren" action="RunPlugin(plugin://plugin.video.viewit/?action=install_ytdlp)" option="close" /> <setting id="install_ytdlp" type="action" label="yt-dlp installieren/reparieren" action="RunPlugin(plugin://plugin.video.viewit/?action=install_ytdlp)" option="close" />
<setting id="ytdlp_status" type="text" label="yt-dlp Status" default="-" enable="false" /> <setting id="ytdlp_status" type="text" label="yt-dlp Status" default="-" enable="false" />
</category> </category>

111
docs/TRAKT.md Normal file
View File

@@ -0,0 +1,111 @@
Trakt in ViewIT Benutzeranleitung
Was ist Trakt?
Trakt (https://trakt.tv) ist ein kostenloser Dienst, der verfolgt welche Serien und Filme du schaust. Damit kannst du:
- Sehen, wo du bei einer Serie aufgehoert hast
- Neue Episoden deiner Serien im Blick behalten
- Deinen kompletten Schauverlauf geraeteuebergreifend synchronisieren
Einrichtung
1) Trakt-Konto erstellen
Falls du noch kein Konto hast, registriere dich kostenlos auf https://trakt.tv/auth/join
2) Trakt in ViewIT aktivieren
- Oeffne ViewIT in Kodi
- Gehe zu Einstellungen (Zahnrad-Symbol oder Kontextmenue)
- Wechsle zur Kategorie "Trakt"
- Setze "Trakt aktivieren" auf An
3) Trakt autorisieren
- Klicke auf "Trakt autorisieren"
- ViewIT zeigt dir einen Code und eine URL an
- Oeffne https://trakt.tv/activate in einem Browser (Handy oder PC)
- Melde dich an und gib den angezeigten Code ein
- Bestaetige die Autorisierung
- ViewIT erkennt die Freigabe automatisch fertig!
Die Autorisierung bleibt dauerhaft gespeichert. Du musst das nur einmal machen.
Einstellungen
- Trakt aktivieren: Schaltet alle Trakt-Funktionen ein oder aus
- Trakt autorisieren: Verbindet ViewIT mit deinem Trakt-Konto
- Scrobbling aktivieren: Sendet automatisch an Trakt, was du gerade schaust
- Geschaute Serien automatisch zur Watchlist hinzufuegen: Fuegt Serien/Filme beim Schauen automatisch zu deiner Trakt-Watchlist hinzu, damit sie bei "Upcoming" erscheinen
Menues im Hauptmenue
Wenn Trakt aktiviert und autorisiert ist, erscheinen im ViewIT-Hauptmenue folgende Eintraege:
Weiterschauen
Zeigt Serien, bei denen du mittendrin aufgehoert hast. Praktisch um schnell dort weiterzumachen, wo du zuletzt warst.
Trakt Upcoming
Zeigt neue Episoden der naechsten 14 Tage fuer alle Serien in deiner Trakt-Watchlist. Die Ansicht ist nach Datum gruppiert:
- Heute Episoden, die heute erscheinen
- Morgen Episoden von morgen
- Wochentag z.B. "Mittwoch", "Donnerstag"
- Wochentag + Datum ab naechster Woche, z.B. "Montag 24.03."
Jeder Eintrag zeigt Serienname, Staffel/Episode und Episodentitel, z.B.:
Game of Thrones S02E05: The Wolf and the Lion
Damit eine Serie hier erscheint, muss sie in deiner Trakt-Watchlist sein. Du kannst Serien auf drei Wegen hinzufuegen:
- Direkt auf trakt.tv
- Ueber das Kontextmenue in der Trakt History (siehe unten)
- Automatisch beim Schauen (Einstellung "Geschaute Serien automatisch zur Watchlist hinzufuegen")
Trakt Watchlist
Zeigt alle Titel in deiner Trakt-Watchlist, unterteilt in Filme und Serien.
Ein Klick auf einen Eintrag fuehrt zur Staffel-/Episodenauswahl in ViewIT.
Trakt History
Zeigt deine zuletzt geschauten Episoden und Filme (seitenweise, neueste zuerst). Jeder Eintrag zeigt Serienname mit Staffel, Episode, Episodentitel und Poster.
Kontextmenue (lange druecken oder Taste "C"):
- "Zur Trakt-Watchlist hinzufuegen" Fuegt die Serie/den Film zu deiner Watchlist hinzu, damit kuenftige Episoden bei "Upcoming" erscheinen
Scrobbling
Scrobbling bedeutet, dass ViewIT automatisch an Trakt meldet was du schaust:
- Du startest eine Episode oder einen Film in ViewIT
- ViewIT sendet "Start" an Trakt (die Episode erscheint als "Watching" in deinem Profil)
- Wenn die Wiedergabe endet, sendet ViewIT "Stop" mit dem Fortschritt
- Hat der Fortschritt mindestens 80% erreicht, markiert Trakt die Episode als gesehen
Das passiert vollautomatisch im Hintergrund du musst nichts tun.
Haeufige Fragen
Warum erscheint eine Serie nicht bei "Upcoming"?
Die Serie muss in deiner Trakt-Watchlist sein. Fuege sie ueber die Trakt History (Kontextmenue) oder direkt auf trakt.tv hinzu.
Warum wird eine Episode nicht als gesehen markiert?
Trakt markiert Episoden erst als gesehen, wenn mindestens ca. 80% geschaut wurden. Wenn du vorher abbrichst, wird sie nicht als gesehen gezaehlt.
Kann ich Trakt auf mehreren Geraeten nutzen?
Ja. Autorisiere ViewIT auf jedem Geraet und alle teilen denselben Schauverlauf ueber dein Trakt-Konto.
Muss ich online sein?
Ja, Trakt benoetigt eine Internetverbindung. Ohne Verbindung funktioniert die Wiedergabe weiterhin, aber Scrobbling und Trakt-Menues sind nicht verfuegbar.