Compare commits

...

11 Commits

11 changed files with 2106 additions and 428 deletions

View File

@@ -1,3 +1,47 @@
## 0.1.84.5-dev - 2026-03-31
- dev: bump to 0.1.84.0-dev SerienStream Sammlungen mit Poster/Plot, Session-Cache für Sammlungs-URLs
## 0.1.84.0-dev - 2026-03-16
- dev: bump to 0.1.83.5-dev Trakt Weiterschauen via watched/shows, Specials überspringen
## 0.1.83.5-dev - 2026-03-15
- dev: SerienStream Suche via /suche?term=, Staffel 0 als Filme, Katalog-Suche entfernt
## 0.1.83.0-dev - 2026-03-15
- dev: Trakt Performance, Suchfilter Phrase-Match, Debug-Settings Expert-Level
## 0.1.82.5-dev - 2026-03-15
- dev: Update-Versionsvergleich numerisch korrigiert
## 0.1.82.0-dev - 2026-03-14
- dev: HDFilme Plot in Rubrik Neuste anzeigen
## 0.1.81.5-dev - 2026-03-14
- dev: YouTube HD via inputstream.adaptive, DokuStreams Suche fix
## 0.1.81.0-dev - 2026-03-14
- dev: YouTube Fixes, Trakt Credentials fest, Upcoming Ansicht, Watchlist Kontextmenue
## 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
- dev: Changelog-Hook auf prepare-commit-msg umgestellt
## 0.1.79.0-dev - 2026-03-11
- dev: TMDB API-Key automatisch aus Kodi-Scraper ermitteln

View File

@@ -1,11 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.79.5-dev" provider-name="ViewIt">
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.85.0-dev" provider-name="ViewIt">
<requires>
<import addon="xbmc.python" version="3.0.0" />
<import addon="script.module.requests" />
<import addon="script.module.beautifulsoup4" />
<import addon="script.module.resolveurl" version="5.1.0" />
<import addon="script.trakt" optional="true" />
<import addon="script.module.yt-dlp" optional="true" />
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video</provides>

View File

@@ -370,6 +370,40 @@ class TraktClient:
return []
return self._parse_history_items(payload)
def get_watched_shows(self, token: str) -> list[TraktItem]:
"""GET /users/me/watched/shows alle Serien mit zuletzt gesehener Episode."""
status, payload = self._get("/users/me/watched/shows", token=token)
if status != 200 or not isinstance(payload, list):
self._do_log(f"get_watched_shows: status={status}")
return []
result: list[TraktItem] = []
for entry in payload:
if not isinstance(entry, dict):
continue
show = entry.get("show") or {}
ids = self._parse_ids((show.get("ids") or {}))
title = str(show.get("title", "") or "")
year = int(show.get("year", 0) or 0)
seasons = entry.get("seasons") or []
last_season = 0
last_episode = 0
for s in seasons:
snum = int((s.get("number") or 0))
if snum == 0: # Specials überspringen
continue
for ep in (s.get("episodes") or []):
enum = int((ep.get("number") or 0))
if snum > last_season or (snum == last_season and enum > last_episode):
last_season = snum
last_episode = enum
if title:
result.append(TraktItem(
title=title, year=year, media_type="episode",
ids=ids, season=last_season, episode=last_episode,
))
self._do_log(f"get_watched_shows: {len(result)} Serien")
return result
# -------------------------------------------------------------------
# Calendar
# -------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -286,7 +286,7 @@ class DokuStreamsPlugin(BasisPlugin):
soup = _get_soup(search_url, session=session)
except Exception:
return []
return _parse_listing_hits(soup, query=query)
return _parse_listing_hits(soup)
def capabilities(self) -> set[str]:
return {"genres", "popular_series", "tags", "random"}
@@ -455,15 +455,24 @@ class DokuStreamsPlugin(BasisPlugin):
art = {"thumb": poster, "poster": poster}
return info, art, None
def series_url_for_title(self, title: str) -> Optional[str]:
return self._title_to_url.get((title or "").strip())
def remember_series_url(self, title: str, url: str) -> None:
title = (title or "").strip()
url = (url or "").strip()
if title and url:
self._title_to_url[title] = url
def seasons_for(self, title: str) -> List[str]:
title = (title or "").strip()
if not title or title not in self._title_to_url:
if not title:
return []
return ["Stream"]
def episodes_for(self, title: str, season: str) -> List[str]:
title = (title or "").strip()
if not title or title not in self._title_to_url:
if not title:
return []
return [title]
@@ -537,6 +546,14 @@ class DokuStreamsPlugin(BasisPlugin):
"""Folgt Redirects und versucht ResolveURL fuer Hoster-Links."""
if not link:
return None
# YouTube-URLs via yt-dlp aufloesen
from ytdlp_helper import extract_youtube_id, resolve_youtube_url
yt_id = extract_youtube_id(link)
if yt_id:
resolved = resolve_youtube_url(yt_id)
if resolved:
return resolved
return None
from plugin_helpers import resolve_via_resolveurl
resolved = resolve_via_resolveurl(link, fallback_to_link=False)
if resolved:

View File

@@ -388,7 +388,7 @@ class HdfilmePlugin(BasisPlugin):
info: dict[str, str] = {"title": title}
art: dict[str, str] = {}
# Cache-Hit
# Cache-Hit nur zurückgeben wenn Plot vorhanden (sonst Detailseite laden)
cached = self._title_meta.get(title)
if cached:
plot, poster = cached
@@ -396,7 +396,7 @@ class HdfilmePlugin(BasisPlugin):
info["plot"] = plot
if poster:
art["thumb"] = art["poster"] = poster
if info or art:
if plot:
return info, art, None
# Detailseite laden

View File

@@ -57,7 +57,6 @@ else: # pragma: no cover
SETTING_BASE_URL = "serienstream_base_url"
SETTING_CATALOG_SEARCH = "serienstream_catalog_search"
DEFAULT_BASE_URL = "https://s.to"
DEFAULT_PREFERRED_HOSTERS = ["voe"]
DEFAULT_TIMEOUT = 20
@@ -80,10 +79,7 @@ HEADERS = {
SESSION_CACHE_TTL_SECONDS = 300
SESSION_CACHE_PREFIX = "viewit.serienstream"
SESSION_CACHE_MAX_TITLE_URLS = 800
CATALOG_SEARCH_TTL_SECONDS = 600
CATALOG_SEARCH_CACHE_KEY = "catalog_index"
GENRE_LIST_PAGE_SIZE = 20
_CATALOG_INDEX_MEMORY: tuple[float, list["SeriesResult"]] = (0.0, [])
ProgressCallback = Optional[Callable[[str, int | None], Any]]
@@ -509,6 +505,14 @@ def _strip_tags(value: str) -> str:
return re.sub(r"<[^>]+>", " ", value or "")
def _clean_collection_title(title: str) -> str:
cleaned = "".join(
ch for ch in title
if unicodedata.category(ch) not in ("So", "Sm", "Sk", "Sc", "Cs", "Co", "Cn")
)
return re.sub(r"\s+", " ", cleaned).strip()
def _search_series_api(query: str) -> list[SeriesResult]:
query = (query or "").strip()
if not query:
@@ -575,8 +579,8 @@ def _search_series_server(query: str) -> list[SeriesResult]:
if not query:
return []
base = _get_base_url()
search_url = f"{base}/search?q={quote(query)}"
alt_url = f"{base}/suche?q={quote(query)}"
search_url = f"{base}/suche?term={quote(query)}"
alt_url = f"{base}/search?term={quote(query)}"
for url in (search_url, alt_url):
try:
body = _get_html_simple(url)
@@ -606,158 +610,30 @@ def _search_series_server(query: str) -> list[SeriesResult]:
continue
seen_urls.add(url_abs)
results.append(SeriesResult(title=title, description="", url=url_abs))
filtered = [r for r in results if _matches_query(query, title=r.title)]
if filtered:
return filtered
if results:
return results
api_results = _search_series_api(query)
if api_results:
return api_results
return []
def _extract_catalog_index_from_html(body: str, *, progress_callback: ProgressCallback = None) -> list[SeriesResult]:
items: list[SeriesResult] = []
if not body:
return items
seen_urls: set[str] = set()
item_re = re.compile(
r"<li[^>]*class=[\"'][^\"']*series-item[^\"']*[\"'][^>]*>(.*?)</li>",
re.IGNORECASE | re.DOTALL,
)
anchor_re = re.compile(r"<a[^>]+href=[\"']([^\"']+)[\"'][^>]*>(.*?)</a>", re.IGNORECASE | re.DOTALL)
data_search_re = re.compile(r"data-search=[\"']([^\"']*)[\"']", re.IGNORECASE)
for idx, match in enumerate(item_re.finditer(body), start=1):
if idx == 1 or idx % 200 == 0:
_emit_progress(progress_callback, f"Katalog parsen {idx}", 62)
block = match.group(0)
inner = match.group(1) or ""
anchor_match = anchor_re.search(inner)
if not anchor_match:
continue
href = (anchor_match.group(1) or "").strip()
url = _absolute_url(href)
if not url or "/serie/" not in url or "/staffel-" in url or "/episode-" in url:
continue
if url in seen_urls:
continue
seen_urls.add(url)
title_raw = anchor_match.group(2) or ""
title = unescape(re.sub(r"\s+", " ", _strip_tags(title_raw))).strip()
if not title:
continue
search_match = data_search_re.search(block)
description = (search_match.group(1) or "").strip() if search_match else ""
items.append(SeriesResult(title=title, description=description, url=url))
return items
def _catalog_index_from_soup(soup: BeautifulSoupT) -> list[SeriesResult]:
items: list[SeriesResult] = []
if not soup:
return items
seen_urls: set[str] = set()
for item in soup.select("li.series-item"):
anchor = item.find("a", href=True)
if not anchor:
continue
href = (anchor.get("href") or "").strip()
url = _absolute_url(href)
if not url or "/serie/" not in url or "/staffel-" in url or "/episode-" in url:
continue
if url in seen_urls:
continue
seen_urls.add(url)
title = (anchor.get_text(" ", strip=True) or "").strip()
if not title:
continue
description = (item.get("data-search") or "").strip()
items.append(SeriesResult(title=title, description=description, url=url))
return items
def _load_catalog_index_from_cache() -> Optional[list[SeriesResult]]:
global _CATALOG_INDEX_MEMORY
expires_at, cached = _CATALOG_INDEX_MEMORY
if cached and expires_at > time.time():
return list(cached)
raw = _session_cache_get(CATALOG_SEARCH_CACHE_KEY)
if not isinstance(raw, list):
return None
items: list[SeriesResult] = []
for entry in raw:
if not isinstance(entry, list) or len(entry) < 2:
continue
title = str(entry[0] or "").strip()
url = str(entry[1] or "").strip()
description = str(entry[2] or "") if len(entry) > 2 else ""
cover = str(entry[3] or "").strip() if len(entry) > 3 else ""
if title and url:
items.append(SeriesResult(title=title, description=description, url=url, cover=cover))
if items:
_CATALOG_INDEX_MEMORY = (time.time() + CATALOG_SEARCH_TTL_SECONDS, list(items))
return items or None
def _store_catalog_index_in_cache(items: list[SeriesResult]) -> None:
global _CATALOG_INDEX_MEMORY
if not items:
return
_CATALOG_INDEX_MEMORY = (time.time() + CATALOG_SEARCH_TTL_SECONDS, list(items))
payload: list[list[str]] = []
for entry in items:
if not entry.title or not entry.url:
continue
payload.append([entry.title, entry.url, entry.description, entry.cover])
_session_cache_set(CATALOG_SEARCH_CACHE_KEY, payload, ttl_seconds=CATALOG_SEARCH_TTL_SECONDS)
def search_series(query: str, *, progress_callback: ProgressCallback = None) -> list[SeriesResult]:
"""Sucht Serien. Katalog-Suche (vollstaendig) oder API-Suche (max 10) je nach Setting."""
"""Sucht Serien. Server-Suche (/suche?term=) zuerst, API als Fallback."""
_ensure_requests()
if not _normalize_search_text(query):
return []
use_catalog = _get_setting_bool(SETTING_CATALOG_SEARCH, default=True)
if use_catalog:
_emit_progress(progress_callback, "Pruefe Such-Cache", 15)
cached = _load_catalog_index_from_cache()
if cached is not None:
matched_from_cache = [entry for entry in cached if entry.title and _matches_query(query, title=entry.title)]
_emit_progress(progress_callback, f"Cache-Treffer: {len(cached)}", 35)
if matched_from_cache:
return matched_from_cache
_emit_progress(progress_callback, "Lade Katalogseite", 42)
catalog_url = f"{_get_base_url()}/serien?by=genre"
items: list[SeriesResult] = []
try:
soup = _get_soup_simple(catalog_url)
items = _catalog_index_from_soup(soup)
except Exception:
body = _get_html_simple(catalog_url)
items = _extract_catalog_index_from_html(body, progress_callback=progress_callback)
if not items:
_emit_progress(progress_callback, "Fallback-Parser", 58)
soup = BeautifulSoup(body, "html.parser")
items = _catalog_index_from_soup(soup)
if items:
_store_catalog_index_in_cache(items)
_emit_progress(progress_callback, f"Filtere Treffer ({len(items)})", 70)
return [entry for entry in items if entry.title and _matches_query(query, title=entry.title)]
# API-Suche (primaer wenn Katalog deaktiviert, Fallback wenn Katalog leer)
_emit_progress(progress_callback, "API-Suche", 60)
api_results = _search_series_api(query)
if api_results:
_emit_progress(progress_callback, f"API-Treffer: {len(api_results)}", 80)
return api_results
_emit_progress(progress_callback, "Server-Suche", 85)
# 1. Server-Suche (schnell, vollstaendig, direkte HTML-Suche)
_emit_progress(progress_callback, "Suche", 20)
server_results = _search_series_server(query)
if server_results:
_emit_progress(progress_callback, f"Server-Treffer: {len(server_results)}", 95)
return [entry for entry in server_results if entry.title and _matches_query(query, title=entry.title)]
return []
return server_results
# 2. API-Suche (Fallback, max 10 Ergebnisse)
_emit_progress(progress_callback, "API-Suche", 60)
return _search_series_api(query)
def parse_series_catalog(soup: BeautifulSoupT) -> dict[str, list[SeriesResult]]:
@@ -1159,6 +1035,8 @@ class SerienstreamPlugin(BasisPlugin):
self._latest_hoster_cache: dict[str, list[str]] = {}
self._series_metadata_cache: dict[str, tuple[dict[str, str], dict[str, str]]] = {}
self._series_metadata_full: set[str] = set()
self._collection_url_cache: dict[str, str] = {}
self._collection_has_more: bool = False
self.is_available = True
self.unavailable_reason: str | None = None
if not self._requests_available: # pragma: no cover - optional dependency
@@ -1252,7 +1130,7 @@ class SerienstreamPlugin(BasisPlugin):
except Exception:
continue
url = str(item.get("url") or "").strip()
if number <= 0 or not url:
if number < 0 or not url:
continue
seasons.append(SeasonInfo(number=number, url=url, episodes=[]))
if not seasons:
@@ -1383,7 +1261,63 @@ class SerienstreamPlugin(BasisPlugin):
def capabilities(self) -> set[str]:
"""Meldet unterstützte Features für Router-Menüs."""
return {"popular_series", "genres", "latest_episodes", "alpha"}
return {"popular_series", "genres", "latest_episodes", "alpha", "collections"}
def collections(self) -> list[str]:
"""Liefert Sammlungs-Namen von /sammlungen (Seite 1, für Paginierung)."""
return self._collections_page(1)
def _collections_page(self, page: int = 1) -> list[str]:
"""Liefert eine Seite mit Sammlungs-Namen von /sammlungen (paginiert)."""
if not self._requests_available:
return []
base = _get_base_url()
names: list[str] = []
url_map: dict[str, str] = {}
url = f"{base}/sammlungen" if page == 1 else f"{base}/sammlungen?page={page}"
soup = _get_soup_simple(url)
for a in soup.select('a[href*="/sammlung/"]'):
h2 = a.find("h2")
if not h2:
continue
title = _clean_collection_title(h2.get_text(strip=True))
href = (a.get("href") or "").strip()
if title and href:
url_map[title] = _absolute_url(href)
names.append(title)
if url_map:
existing = _session_cache_get("collection_urls")
if isinstance(existing, dict):
existing.update(url_map)
_session_cache_set("collection_urls", existing)
else:
_session_cache_set("collection_urls", url_map)
names.sort(key=lambda t: t.casefold())
return names
def titles_for_collection(self, collection: str, page: int = 1) -> list[str]:
"""Liefert Serien-Titel einer Sammlung (paginiert)."""
if not self._requests_available:
return []
url_map = _session_cache_get("collection_urls")
if isinstance(url_map, dict):
self._collection_url_cache.update(url_map)
url = self._collection_url_cache.get(collection)
if not url:
return []
if page > 1:
url = f"{url}?page={page}"
base_url = self._collection_url_cache[collection]
soup = _get_soup_simple(url)
titles: list[str] = []
for a in soup.select('h6 a[href*="/serie/"]'):
title = a.get_text(strip=True)
href = (a.get("href") or "").strip()
if title and href:
self._remember_series_result(title, _absolute_url(href), "")
titles.append(title)
self._collection_has_more = bool(soup.select(f'a[href*="?page={page + 1}"]'))
return titles
def popular_series(self) -> list[str]:
"""Liefert die Titel der beliebten Serien (Quelle: `/beliebte-serien`)."""
@@ -1794,6 +1728,8 @@ class SerienstreamPlugin(BasisPlugin):
@staticmethod
def _season_label(number: int) -> str:
if number == 0:
return "Filme"
return f"Staffel {number}"
@staticmethod
@@ -1808,6 +1744,8 @@ class SerienstreamPlugin(BasisPlugin):
@staticmethod
def _parse_season_number(label: str) -> int | None:
if (label or "").strip().casefold() == "filme":
return 0
digits = "".join(ch for ch in label if ch.isdigit())
if not digits:
return None

View File

@@ -0,0 +1,238 @@
"""YouTube Plugin fuer ViewIT.
Suche und Wiedergabe von YouTube-Videos via HTML-Scraping und yt-dlp.
Benoetigt script.module.yt-dlp (optional).
Video-Eintraege werden als "Titel||VIDEO_ID" kodiert.
"""
from __future__ import annotations
import json
import re
from typing import Any, Callable, Dict, List, Optional, Set
try:
import requests
except ImportError:
requests = None # type: ignore
from plugin_interface import BasisPlugin
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
# ---------------------------------------------------------------------------
DEFAULT_TIMEOUT = 20
_SEP = "||" # Trennzeichen zwischen Titel und Video-ID
BASE_URL = "https://www.youtube.com"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
),
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
ProgressCallback = Optional[Callable[[str, Optional[int]], Any]]
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _encode(title: str, video_id: str) -> str:
return f"{title}{_SEP}{video_id}"
def _decode_id(entry: str) -> Optional[str]:
"""Extrahiert Video-ID aus einem kodierten Eintrag."""
if _SEP in entry:
return entry.split(_SEP, 1)[1].strip()
# Fallback: 11-Zeichen YouTube-ID am Ende
m = re.search(r"([A-Za-z0-9_-]{11})$", entry)
return m.group(1) if m else None
def _decode_title(entry: str) -> str:
if _SEP in entry:
return entry.split(_SEP, 1)[0].strip()
return entry
def _get_session() -> Any:
try:
from http_session_pool import get_requests_session
return get_requests_session("youtube", headers=HEADERS)
except Exception:
if requests:
s = requests.Session()
s.headers.update(HEADERS)
return s
return None
def _extract_yt_initial_data(html: str) -> Optional[dict]:
"""Extrahiert ytInitialData JSON aus dem HTML-Source."""
m = re.search(r"var ytInitialData\s*=\s*(\{.*?\});\s*(?:var |</script>)", html, re.DOTALL)
if not m:
# Alternativer Pattern
m = re.search(r"ytInitialData\s*=\s*(\{.+?\})\s*;", html, re.DOTALL)
if not m:
return None
try:
return json.loads(m.group(1))
except Exception:
return None
def _videos_from_search_data(data: dict) -> List[str]:
"""Extrahiert Video-Eintraege aus ytInitialData (Suchergebnisse)."""
results: List[str] = []
try:
contents = (
data
.get("contents", {})
.get("twoColumnSearchResultsRenderer", {})
.get("primaryContents", {})
.get("sectionListRenderer", {})
.get("contents", [])
)
for section in contents:
items = (
section
.get("itemSectionRenderer", {})
.get("contents", [])
)
for item in items:
vr = item.get("videoRenderer") or item.get("compactVideoRenderer")
if not vr:
continue
video_id = vr.get("videoId", "").strip()
if not video_id:
continue
title_runs = vr.get("title", {}).get("runs", [])
title = "".join(r.get("text", "") for r in title_runs).strip()
if not title:
title = vr.get("title", {}).get("simpleText", "").strip()
if title and video_id:
results.append(_encode(title, video_id))
except Exception as exc:
_log(f"[YouTube] _videos_from_search_data Fehler: {exc}")
return results
def _search_with_ytdlp(query: str, count: int = 20) -> List[str]:
"""Sucht YouTube-Videos via yt-dlp ytsearch-Extraktor."""
if not ensure_ytdlp_in_path():
return []
try:
from yt_dlp import YoutubeDL # type: ignore
except ImportError:
return []
ydl_opts = {"quiet": True, "no_warnings": True, "extract_flat": True}
try:
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(f"ytsearch{count}:{query}", download=False)
if not info:
return []
return [
_encode(e["title"], e["id"])
for e in (info.get("entries") or [])
if e.get("id") and e.get("title")
]
except Exception as exc:
_log(f"[YouTube] yt-dlp Suche Fehler: {exc}")
return []
def _fetch_search_videos(url: str) -> List[str]:
"""Holt Videos von einer YouTube-Suche via ytInitialData."""
session = _get_session()
if session is None:
return []
try:
resp = session.get(url, timeout=DEFAULT_TIMEOUT)
resp.raise_for_status()
data = _extract_yt_initial_data(resp.text)
if not data:
return []
return _videos_from_search_data(data)
except Exception as exc:
_log(f"[YouTube] _fetch_search_videos ({url}): {exc}")
return []
from ytdlp_helper import ensure_ytdlp_in_path, resolve_youtube_url
# ---------------------------------------------------------------------------
# Plugin
# ---------------------------------------------------------------------------
class YoutubePlugin(BasisPlugin):
name = "YouTube"
_SEASONS = ["Stream"]
def capabilities(self) -> Set[str]:
return set()
async def search_titles(
self,
query: str,
progress_callback: ProgressCallback = None,
) -> List[str]:
if not query.strip():
return []
# Primär: yt-dlp (robust, kein HTML-Scraping)
results = _search_with_ytdlp(query)
if results:
return results
# Fallback: HTML-Scraping
if requests is None:
return []
url = f"{BASE_URL}/results?search_query={requests.utils.quote(query)}" # type: ignore
return _fetch_search_videos(url)
def seasons_for(self, title: str) -> List[str]:
return list(self._SEASONS)
def episodes_for(self, title: str, season: str) -> List[str]:
if season == "Stream":
return [title]
return []
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
video_id = _decode_id(episode) or _decode_id(title)
if not video_id:
return None
return resolve_youtube_url(video_id)
def resolve_stream_link(self, link: str) -> Optional[str]:
return link # bereits direkte URL
def metadata_for(self, title: str):
"""Thumbnail aus Video-ID ableiten."""
video_id = _decode_id(title)
clean_title = _decode_title(title)
info: Dict[str, str] = {"title": clean_title}
art: Dict[str, str] = {}
if video_id:
art["thumb"] = f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg"
art["poster"] = f"https://i.ytimg.com/vi/{video_id}/maxresdefault.jpg"
return info, art, None
Plugin = YoutubePlugin

View File

@@ -1,126 +1,861 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<category label="Quellen">
<setting id="serienstream_base_url" type="text" label="SerienStream Basis-URL" default="https://s.to" />
<setting id="serienstream_catalog_search" type="bool" label="SerienStream: Katalog-Suche (mehr Ergebnisse, langsamer)" default="true" />
<setting id="aniworld_base_url" type="text" label="AniWorld Basis-URL" default="https://aniworld.to" />
<setting id="topstream_base_url" type="text" label="TopStream Basis-URL" default="https://topstreamfilm.live" />
<setting id="einschalten_base_url" type="text" label="Einschalten Basis-URL" default="https://einschalten.in" />
<setting id="filmpalast_base_url" type="text" label="Filmpalast Basis-URL" default="https://filmpalast.to" />
<setting id="doku_streams_base_url" type="text" label="Doku-Streams Basis-URL" default="https://doku-streams.com" />
</category>
<settings version="1">
<section id="plugin.video.viewit">
<category label="Metadaten">
<setting id="serienstream_metadata_source" type="enum" label="SerienStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="aniworld_metadata_source" type="enum" label="AniWorld Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="topstreamfilm_metadata_source" type="enum" label="TopStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="einschalten_metadata_source" type="enum" label="Einschalten Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="filmpalast_metadata_source" type="enum" label="Filmpalast Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="doku_streams_metadata_source" type="enum" label="Doku-Streams Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="kkiste_metadata_source" type="enum" label="KKiste Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="moflix_metadata_source" type="enum" label="Moflix Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
<setting id="tmdb_enabled" type="bool" label="TMDB aktivieren" default="true" />
<setting id="tmdb_language" type="text" label="TMDB Sprache (z. B. de-DE)" default="de-DE" />
<setting id="tmdb_show_plot" type="bool" label="TMDB Beschreibung anzeigen" default="true" />
<setting id="tmdb_show_art" type="bool" label="TMDB Poster und Vorschaubild anzeigen" default="true" />
<setting id="tmdb_show_fanart" type="bool" label="TMDB Fanart/Backdrop anzeigen" default="true" />
<setting id="tmdb_show_rating" type="bool" label="TMDB Bewertung anzeigen" default="true" />
<setting id="tmdb_show_votes" type="bool" label="TMDB Stimmen anzeigen" default="false" />
</category>
<!-- ═══════════════════════════════════════════ Wiedergabe -->
<category id="playback" label="Wiedergabe">
<group id="1">
<setting id="autoplay_enabled" type="boolean" label="Autoplay (bevorzugten Hoster automatisch waehlen)">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="preferred_hoster" type="integer" label="Bevorzugter Hoster">
<level>0</level>
<default>0</default>
<constraints>
<options>
<option label="voe">0</option>
<option label="streamtape">1</option>
<option label="doodstream">2</option>
<option label="vidoza">3</option>
<option label="mixdrop">4</option>
<option label="supervideo">5</option>
<option label="dropload">6</option>
</options>
</constraints>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="autoplay_enabled">true</condition>
</dependency>
</dependencies>
<control type="spinner" format="integer"/>
</setting>
</group>
</category>
<category label="TMDB Erweitert">
<setting id="tmdb_api_key" type="text" label="TMDB API Key (optional)" default="" />
<setting id="tmdb_api_key_active" type="text" label="Aktiver TMDB API Key" default="" />
<setting id="tmdb_prefetch_concurrency" type="number" label="TMDB: gleichzeitige Anfragen (1-20)" default="6" />
<setting id="tmdb_show_cast" type="bool" label="TMDB Besetzung anzeigen" default="false" />
<setting id="tmdb_show_episode_cast" type="bool" label="TMDB Besetzung pro Episode anzeigen" default="false" />
<setting id="tmdb_genre_metadata" type="bool" label="TMDB Daten in Genre-Listen anzeigen" default="false" />
<setting id="tmdb_log_requests" type="bool" label="TMDB API-Anfragen loggen" default="false" />
<setting id="tmdb_log_responses" type="bool" label="TMDB API-Antworten loggen" default="false" />
</category>
<!-- ═══════════════════════════════════════════ Trakt -->
<category id="trakt" label="Trakt">
<group id="1">
<setting id="trakt_enabled" type="boolean" label="Trakt aktivieren">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="trakt_status" type="string" label="Status">
<level>0</level>
<default>Nicht verbunden</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string">
<heading>Status</heading>
</control>
</setting>
<setting id="trakt_auth" type="action" label="Trakt autorisieren">
<level>0</level>
<data>RunPlugin(plugin://plugin.video.viewit/?action=trakt_auth)</data>
<control type="button" format="action"/>
</setting>
<setting id="trakt_scrobble" type="boolean" label="Scrobbling aktivieren">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="trakt_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="trakt_auto_watchlist" type="boolean" label="Geschaute Serien automatisch zur Watchlist hinzufuegen">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="trakt_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
</group>
<group id="2">
<setting id="trakt_access_token" type="string" label="">
<level>0</level>
<default/>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="trakt_refresh_token" type="string" label="">
<level>0</level>
<default/>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="trakt_token_expires" type="string" label="">
<level>0</level>
<default>0</default>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
</group>
</category>
<category label="Anzeige">
<setting id="filmpalast_max_page_items" type="number" label="Filmpalast: Max. Eintraege pro Seite" default="15" />
<setting id="topstreamfilm_max_page_items" type="number" label="TopStream: Max. Eintraege pro Seite" default="15" />
<setting id="aniworld_max_page_items" type="number" label="AniWorld: Max. Eintraege pro Seite" default="15" />
<setting id="netzkkino_max_page_items" type="number" label="Netzkino: Max. Eintraege pro Seite" default="15" />
<setting id="kkiste_max_page_items" type="number" label="KKiste: Max. Eintraege pro Seite" default="15" />
<setting id="hdfilme_max_page_items" type="number" label="HDFilme: Max. Eintraege pro Seite" default="15" />
<setting id="moflix_max_page_items" type="number" label="Moflix: Max. Eintraege pro Seite" default="15" />
<setting id="einschalten_max_page_items" type="number" label="Einschalten: Max. Eintraege pro Seite" default="15" />
</category>
<!-- ═══════════════════════════════════════════ Metadaten -->
<category id="metadata" label="Metadaten">
<group id="1">
<setting id="tmdb_enabled" type="boolean" label="TMDB aktivieren">
<level>0</level>
<default>true</default>
<control type="toggle"/>
</setting>
<setting id="tmdb_language" type="string" label="TMDB Sprache (z. B. de-DE)">
<level>0</level>
<default>de-DE</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="edit" format="string">
<heading>TMDB Sprache</heading>
</control>
</setting>
<setting id="tmdb_show_plot" type="boolean" label="Beschreibung anzeigen">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="tmdb_show_art" type="boolean" label="Poster und Vorschaubild anzeigen">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="tmdb_show_fanart" type="boolean" label="Fanart/Backdrop anzeigen">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="tmdb_show_rating" type="boolean" label="Bewertung anzeigen">
<level>0</level>
<default>true</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="tmdb_show_votes" type="boolean" label="Stimmen anzeigen">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="tmdb_enabled">true</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
</group>
<group id="2">
<setting id="serienstream_metadata_source" type="integer" label="SerienStream Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="aniworld_metadata_source" type="integer" label="AniWorld Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="topstreamfilm_metadata_source" type="integer" label="TopStream Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="einschalten_metadata_source" type="integer" label="Einschalten Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="filmpalast_metadata_source" type="integer" label="Filmpalast Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="doku_streams_metadata_source" type="integer" label="Doku-Streams Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="kkiste_metadata_source" type="integer" label="KKiste Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="moflix_metadata_source" type="integer" label="Moflix Metadatenquelle">
<level>2</level>
<default>0</default>
<constraints>
<options>
<option label="Automatisch">0</option>
<option label="Quelle">1</option>
<option label="TMDB">2</option>
<option label="Mischen">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
</group>
</category>
<category label="Wiedergabe">
<setting id="autoplay_enabled" type="bool" label="Autoplay (bevorzugten Hoster automatisch waehlen)" default="false" />
<setting id="preferred_hoster" type="text" label="Bevorzugter Hoster" default="voe" />
</category>
<!-- ═══════════════════════════════════════════ Anzeige -->
<category id="display" label="Anzeige">
<group id="1">
<setting id="filmpalast_max_page_items" type="integer" label="Filmpalast: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="topstreamfilm_max_page_items" type="integer" label="TopStream: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="aniworld_max_page_items" type="integer" label="AniWorld: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="netzkkino_max_page_items" type="integer" label="Netzkino: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="kkiste_max_page_items" type="integer" label="KKiste: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="hdfilme_max_page_items" type="integer" label="HDFilme: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="moflix_max_page_items" type="integer" label="Moflix: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="einschalten_max_page_items" type="integer" label="Einschalten: Max. Eintraege pro Seite">
<level>2</level>
<default>15</default>
<constraints>
<minimum>5</minimum>
<step>5</step>
<maximum>100</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
</group>
</category>
<category label="Updates">
<setting id="update_channel" type="enum" label="Update-Kanal" default="1" values="Main|Nightly|Custom|Dev" />
<setting id="apply_update_channel" type="action" label="Update-Kanal jetzt anwenden" action="RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel)" option="close" />
<setting id="auto_update_enabled" type="bool" label="Automatische Updates (beim Start pruefen)" default="false" />
<setting id="auto_update_interval" type="enum" label="Update-Pruefintervall" default="1" values="1 Stunde|6 Stunden|24 Stunden" />
<setting id="select_update_version" type="action" label="Version waehlen und installieren" action="RunPlugin(plugin://plugin.video.viewit/?action=select_update_version)" option="close" />
<setting id="install_resolveurl" type="action" label="ResolveURL installieren/reparieren" action="RunPlugin(plugin://plugin.video.viewit/?action=install_resolveurl)" option="close" />
<setting id="resolveurl_auto_install" type="bool" label="ResolveURL automatisch installieren (beim Start pruefen)" default="true" />
<setting id="update_installed_version" type="text" label="Installierte Version" default="-" enable="false" />
<setting id="update_available_selected" type="text" label="Verfuegbar (gewaehlter Kanal)" default="-" enable="false" />
<setting id="resolveurl_status" type="text" label="ResolveURL Status" default="-" enable="false" />
<setting id="update_active_channel" type="text" label="Aktiver Kanal" default="-" enable="false" />
<setting id="update_active_repo_url" type="text" label="Aktive Repo URL" default="-" enable="false" />
<setting id="update_repo_url_main" type="text" label="Main URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
<setting id="update_repo_url_nightly" type="text" label="Nightly URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml" />
<setting id="update_repo_url_dev" type="text" label="Dev URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/dev/addons.xml" />
<setting id="update_repo_url" type="text" label="Custom URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml" />
<setting id="auto_update_last_ts" type="text" label="Auto-Update letzte Pruefung (intern)" default="0" visible="false" />
<setting id="resolveurl_last_ts" type="text" label="ResolveURL letzte Pruefung (intern)" default="0" visible="false" />
</category>
<!-- ═══════════════════════════════════════════ Updates -->
<category id="updates" label="Updates">
<group id="1">
<setting id="update_available_flag" type="boolean" label="">
<level>0</level>
<default>false</default>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="toggle"/>
</setting>
<setting id="update_installed_version" type="string" label="Installierte Version">
<level>0</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="update_available_selected" type="string" label="Neue Version verfuegbar">
<level>0</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
<dependency type="visible">
<condition operator="is" setting="update_available_flag">true</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="update_channel" type="integer" label="Update-Kanal">
<level>0</level>
<default>1</default>
<constraints>
<options>
<option label="Main">0</option>
<option label="Nightly">1</option>
<option label="Custom">2</option>
<option label="Dev">3</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="apply_update_channel" type="action" label="Update-Kanal jetzt anwenden">
<level>0</level>
<data>RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel)</data>
<control type="button" format="action"/>
</setting>
<setting id="auto_update_enabled" type="boolean" label="Automatische Updates (beim Start pruefen)">
<level>0</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="auto_update_interval" type="integer" label="Update-Pruefintervall">
<level>0</level>
<default>1</default>
<constraints>
<options>
<option label="1 Stunde">0</option>
<option label="6 Stunden">1</option>
<option label="24 Stunden">2</option>
</options>
</constraints>
<dependencies>
<dependency type="enable">
<condition operator="is" setting="auto_update_enabled">true</condition>
</dependency>
</dependencies>
<control type="spinner" format="integer"/>
</setting>
<setting id="select_update_version" type="action" label="Version waehlen und installieren">
<level>0</level>
<data>RunPlugin(plugin://plugin.video.viewit/?action=select_update_version)</data>
<control type="button" format="action"/>
</setting>
<setting id="install_resolveurl" type="action" label="ResolveURL installieren/reparieren">
<level>0</level>
<data>RunPlugin(plugin://plugin.video.viewit/?action=install_resolveurl)</data>
<control type="button" format="action"/>
</setting>
<setting id="resolveurl_auto_install" type="boolean" label="ResolveURL automatisch installieren (beim Start pruefen)">
<level>0</level>
<default>true</default>
<control type="toggle"/>
</setting>
<setting id="resolveurl_status" type="string" label="ResolveURL Status">
<level>2</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
</group>
<group id="2">
<setting id="update_active_channel" type="string" label="Aktiver Kanal">
<level>3</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="update_active_repo_url" type="string" label="Aktive Repo URL">
<level>3</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="update_repo_url_main" type="string" label="Main URL (addons.xml)">
<level>3</level>
<default>https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml</default>
<control type="edit" format="string">
<heading>Main URL</heading>
</control>
</setting>
<setting id="update_repo_url_nightly" type="string" label="Nightly URL (addons.xml)">
<level>3</level>
<default>https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml</default>
<control type="edit" format="string">
<heading>Nightly URL</heading>
</control>
</setting>
<setting id="update_repo_url_dev" type="string" label="Dev URL (addons.xml)">
<level>3</level>
<default>https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/dev/addons.xml</default>
<control type="edit" format="string">
<heading>Dev URL</heading>
</control>
</setting>
<setting id="update_repo_url" type="string" label="Custom URL (addons.xml)">
<level>3</level>
<default>https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml</default>
<control type="edit" format="string">
<heading>Custom URL</heading>
</control>
</setting>
<setting id="auto_update_last_ts" type="string" label="">
<level>0</level>
<default>0</default>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="resolveurl_last_ts" type="string" label="">
<level>0</level>
<default>0</default>
<dependencies>
<dependency type="visible">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
</group>
</category>
<category label="Trakt">
<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_scrobble" type="bool" label="Scrobbling aktivieren" default="true" />
<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_token_expires" type="text" label="" default="0" visible="false" />
</category>
<!-- ═══════════════════════════════════════════ YouTube -->
<category id="youtube" label="YouTube">
<group id="1">
<setting id="youtube_quality" type="integer" label="YouTube Videoqualitaet">
<level>0</level>
<default>0</default>
<constraints>
<options>
<option label="Beste">0</option>
<option label="1080p">1</option>
<option label="720p">2</option>
<option label="480p">3</option>
<option label="360p">4</option>
</options>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="install_ytdlp" type="action" label="yt-dlp installieren/reparieren">
<level>0</level>
<data>RunPlugin(plugin://plugin.video.viewit/?action=install_ytdlp)</data>
<control type="button" format="action"/>
</setting>
<setting id="ytdlp_status" type="string" label="yt-dlp Status">
<level>2</level>
<default>-</default>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
</group>
</category>
<category label="Debug Global">
<setting id="debug_log_urls" type="bool" label="URLs mitschreiben (global)" default="false" />
<setting id="debug_dump_html" type="bool" label="HTML speichern (global)" default="false" />
<setting id="debug_show_url_info" type="bool" label="Aktuelle URL anzeigen (global)" default="false" />
<setting id="debug_log_errors" type="bool" label="Fehler mitschreiben (global)" default="false" />
<setting id="log_max_mb" type="number" label="URL-Log: maximale Dateigroesse (MB)" default="5" />
<setting id="log_max_files" type="number" label="URL-Log: Anzahl alter Dateien" default="3" />
<setting id="dump_max_files" type="number" label="HTML: maximale Dateien pro Plugin" default="200" />
</category>
<!-- ═══════════════════════════════════════════ Quellen -->
<category id="sources" label="Quellen">
<group id="1">
<setting id="serienstream_base_url" type="string" label="SerienStream Basis-URL">
<level>3</level>
<default>https://s.to</default>
<control type="edit" format="string">
<heading>SerienStream URL</heading>
</control>
</setting>
<setting id="aniworld_base_url" type="string" label="AniWorld Basis-URL">
<level>3</level>
<default>https://aniworld.to</default>
<control type="edit" format="string">
<heading>AniWorld URL</heading>
</control>
</setting>
<setting id="topstream_base_url" type="string" label="TopStream Basis-URL">
<level>3</level>
<default>https://topstreamfilm.live</default>
<control type="edit" format="string">
<heading>TopStream URL</heading>
</control>
</setting>
<setting id="einschalten_base_url" type="string" label="Einschalten Basis-URL">
<level>3</level>
<default>https://einschalten.in</default>
<control type="edit" format="string">
<heading>Einschalten URL</heading>
</control>
</setting>
<setting id="filmpalast_base_url" type="string" label="Filmpalast Basis-URL">
<level>3</level>
<default>https://filmpalast.to</default>
<control type="edit" format="string">
<heading>Filmpalast URL</heading>
</control>
</setting>
<setting id="doku_streams_base_url" type="string" label="Doku-Streams Basis-URL">
<level>3</level>
<default>https://doku-streams.com</default>
<control type="edit" format="string">
<heading>Doku-Streams URL</heading>
</control>
</setting>
</group>
</category>
<category label="Debug Quellen">
<setting id="log_urls_serienstream" type="bool" label="SerienStream: URLs mitschreiben" default="false" />
<setting id="dump_html_serienstream" type="bool" label="SerienStream: HTML speichern" default="false" />
<setting id="show_url_info_serienstream" type="bool" label="SerienStream: Aktuelle URL anzeigen" default="false" />
<setting id="log_errors_serienstream" type="bool" label="SerienStream: Fehler mitschreiben" default="false" />
<!-- ═══════════════════════════════════════════ TMDB Erweitert -->
<category id="tmdb_advanced" label="TMDB Erweitert">
<group id="1">
<setting id="tmdb_api_key" type="string" label="TMDB API Key (optional)">
<level>3</level>
<default/>
<constraints>
<allowempty>true</allowempty>
</constraints>
<control type="edit" format="string">
<heading>TMDB API Key</heading>
</control>
</setting>
<setting id="tmdb_api_key_active" type="string" label="Aktiver TMDB API Key">
<level>3</level>
<default/>
<dependencies>
<dependency type="enable">
<condition on="property" name="InfoBool">false</condition>
</dependency>
</dependencies>
<control type="edit" format="string"><heading/></control>
</setting>
<setting id="tmdb_prefetch_concurrency" type="integer" label="TMDB: gleichzeitige Anfragen (1-20)">
<level>3</level>
<default>6</default>
<constraints>
<minimum>1</minimum>
<step>1</step>
<maximum>20</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="tmdb_log_requests" type="boolean" label="TMDB API-Anfragen loggen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="tmdb_log_responses" type="boolean" label="TMDB API-Antworten loggen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
</group>
<group id="2">
<setting id="tmdb_show_cast" type="boolean" label="TMDB Besetzung anzeigen">
<level>2</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="tmdb_show_episode_cast" type="boolean" label="TMDB Besetzung pro Episode anzeigen">
<level>2</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="tmdb_genre_metadata" type="boolean" label="TMDB Daten in Genre-Listen anzeigen">
<level>2</level>
<default>false</default>
<control type="toggle"/>
</setting>
</group>
</category>
<setting id="log_urls_aniworld" type="bool" label="AniWorld: URLs mitschreiben" default="false" />
<setting id="dump_html_aniworld" type="bool" label="AniWorld: HTML speichern" default="false" />
<setting id="show_url_info_aniworld" type="bool" label="AniWorld: Aktuelle URL anzeigen" default="false" />
<setting id="log_errors_aniworld" type="bool" label="AniWorld: Fehler mitschreiben" default="false" />
<!-- ═══════════════════════════════════════════ Debug -->
<category id="debug" label="Debug">
<group id="1">
<setting id="debug_log_urls" type="boolean" label="URLs mitschreiben (global)">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="debug_dump_html" type="boolean" label="HTML speichern (global)">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="debug_show_url_info" type="boolean" label="Aktuelle URL anzeigen (global)">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="debug_log_errors" type="boolean" label="Fehler mitschreiben (global)">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_max_mb" type="integer" label="URL-Log: maximale Dateigroesse (MB)">
<level>3</level>
<default>5</default>
<constraints>
<minimum>1</minimum>
<step>1</step>
<maximum>50</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="log_max_files" type="integer" label="URL-Log: Anzahl alter Dateien">
<level>3</level>
<default>3</default>
<constraints>
<minimum>1</minimum>
<step>1</step>
<maximum>20</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
<setting id="dump_max_files" type="integer" label="HTML: maximale Dateien pro Plugin">
<level>3</level>
<default>200</default>
<constraints>
<minimum>10</minimum>
<step>10</step>
<maximum>1000</maximum>
</constraints>
<control type="spinner" format="integer"/>
</setting>
</group>
<group id="2">
<setting id="log_urls_serienstream" type="boolean" label="SerienStream: URLs mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="dump_html_serienstream" type="boolean" label="SerienStream: HTML speichern">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="show_url_info_serienstream" type="boolean" label="SerienStream: Aktuelle URL anzeigen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_errors_serienstream" type="boolean" label="SerienStream: Fehler mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_urls_aniworld" type="boolean" label="AniWorld: URLs mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="dump_html_aniworld" type="boolean" label="AniWorld: HTML speichern">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="show_url_info_aniworld" type="boolean" label="AniWorld: Aktuelle URL anzeigen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_errors_aniworld" type="boolean" label="AniWorld: Fehler mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_urls_topstreamfilm" type="boolean" label="TopStream: URLs mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="dump_html_topstreamfilm" type="boolean" label="TopStream: HTML speichern">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="show_url_info_topstreamfilm" type="boolean" label="TopStream: Aktuelle URL anzeigen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_errors_topstreamfilm" type="boolean" label="TopStream: Fehler mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_urls_einschalten" type="boolean" label="Einschalten: URLs mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="dump_html_einschalten" type="boolean" label="Einschalten: HTML speichern">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="show_url_info_einschalten" type="boolean" label="Einschalten: Aktuelle URL anzeigen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_errors_einschalten" type="boolean" label="Einschalten: Fehler mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_urls_filmpalast" type="boolean" label="Filmpalast: URLs mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="dump_html_filmpalast" type="boolean" label="Filmpalast: HTML speichern">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="show_url_info_filmpalast" type="boolean" label="Filmpalast: Aktuelle URL anzeigen">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
<setting id="log_errors_filmpalast" type="boolean" label="Filmpalast: Fehler mitschreiben">
<level>3</level>
<default>false</default>
<control type="toggle"/>
</setting>
</group>
</category>
<setting id="log_urls_topstreamfilm" type="bool" label="TopStream: URLs mitschreiben" default="false" />
<setting id="dump_html_topstreamfilm" type="bool" label="TopStream: HTML speichern" default="false" />
<setting id="show_url_info_topstreamfilm" type="bool" label="TopStream: Aktuelle URL anzeigen" default="false" />
<setting id="log_errors_topstreamfilm" type="bool" label="TopStream: Fehler mitschreiben" default="false" />
<setting id="log_urls_einschalten" type="bool" label="Einschalten: URLs mitschreiben" default="false" />
<setting id="dump_html_einschalten" type="bool" label="Einschalten: HTML speichern" default="false" />
<setting id="show_url_info_einschalten" type="bool" label="Einschalten: Aktuelle URL anzeigen" default="false" />
<setting id="log_errors_einschalten" type="bool" label="Einschalten: Fehler mitschreiben" default="false" />
<setting id="log_urls_filmpalast" type="bool" label="Filmpalast: URLs mitschreiben" default="false" />
<setting id="dump_html_filmpalast" type="bool" label="Filmpalast: HTML speichern" default="false" />
<setting id="show_url_info_filmpalast" type="bool" label="Filmpalast: Aktuelle URL anzeigen" default="false" />
<setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" />
</category>
</section>
</settings>

185
addon/ytdlp_helper.py Normal file
View File

@@ -0,0 +1,185 @@
"""Gemeinsame yt-dlp Hilfsfunktionen fuer YouTube-Wiedergabe.
Wird von youtube_plugin und dokustreams_plugin genutzt.
"""
from __future__ import annotations
import re
from typing import Any, Dict, Optional
try:
import xbmc # type: ignore
def _log(msg: str) -> None:
xbmc.log(f"[ViewIt][yt-dlp] {msg}", xbmc.LOGWARNING)
except ImportError:
def _log(msg: str) -> None:
pass
_YT_ID_RE = re.compile(
r"(?:youtube(?:-nocookie)?\.com/(?:embed/|v/|watch\?.*?v=)|youtu\.be/)"
r"([A-Za-z0-9_-]{11})"
)
def extract_youtube_id(url: str) -> Optional[str]:
"""Extrahiert eine YouTube Video-ID aus verschiedenen URL-Formaten."""
if not url:
return None
m = _YT_ID_RE.search(url)
return m.group(1) if m else None
def _fix_strptime() -> None:
"""Kodi-Workaround: datetime.strptime Race Condition vermeiden.
Kodi's eingebetteter Python kann in Multi-Thread-Umgebungen dazu fuehren
dass der lazy _strptime-Import fehlschlaegt. Wir importieren das Modul
direkt, damit es beim yt-dlp Aufruf bereits geladen ist.
"""
try:
import _strptime # noqa: F401 erzwingt den internen Import
except Exception:
pass
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": "bestvideo[ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"1": "bestvideo[height<=1080][ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best",
"2": "bestvideo[height<=720][ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]/best",
"3": "bestvideo[height<=480][ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]/best",
"4": "bestvideo[height<=360][ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/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"]
_AUDIO_SEP = "||AUDIO||"
_META_SEP = "||META||"
def resolve_youtube_url(video_id: str) -> Optional[str]:
"""Loest eine YouTube Video-ID via yt-dlp zu einer direkten Stream-URL auf.
Bei getrennten Video+Audio-Streams wird der Rueckgabestring im Format
``video_url||AUDIO||audio_url||META||key=val,key=val,...`` kodiert.
Der Aufrufer kann mit ``split_video_audio()`` alle Teile trennen.
"""
if not ensure_ytdlp_in_path():
_log("yt-dlp nicht verfuegbar (script.module.yt-dlp fehlt)")
try:
import xbmcgui # type: ignore
xbmcgui.Dialog().notification(
"yt-dlp fehlt",
"Bitte yt-dlp in den ViewIT-Einstellungen installieren.",
xbmcgui.NOTIFICATION_ERROR,
5000,
)
except Exception:
pass
return None
try:
from yt_dlp import YoutubeDL # type: ignore
except ImportError:
return None
url = f"https://www.youtube.com/watch?v={video_id}"
fmt = get_quality_format()
ydl_opts: Dict[str, Any] = {
"format": fmt,
"quiet": True,
"no_warnings": True,
"extract_flat": False,
}
try:
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
if not info:
return None
duration = int(info.get("duration") or 0)
# Einzelne URL (kombinierter Stream)
direct = info.get("url")
if direct:
return direct
# Getrennte Video+Audio-Streams (hoehere Qualitaet)
rf = info.get("requested_formats")
if rf and len(rf) >= 2:
vf, af = rf[0], rf[1]
video_url = vf.get("url")
audio_url = af.get("url")
if video_url and audio_url:
vcodec = vf.get("vcodec") or "avc1.640028"
acodec = af.get("acodec") or "mp4a.40.2"
w = int(vf.get("width") or 1920)
h = int(vf.get("height") or 1080)
fps = int(vf.get("fps") or 25)
vbr = int((vf.get("tbr") or 5000) * 1000)
abr = int((af.get("tbr") or 128) * 1000)
asr = int(af.get("asr") or 44100)
ach = int(af.get("audio_channels") or 2)
meta = (
f"vc={vcodec},ac={acodec},"
f"w={w},h={h},fps={fps},"
f"vbr={vbr},abr={abr},"
f"asr={asr},ach={ach},dur={duration}"
)
_log(f"Getrennte Streams: {h}p {vcodec} + {acodec}")
return f"{video_url}{_AUDIO_SEP}{audio_url}{_META_SEP}{meta}"
if video_url:
return video_url
# Fallback: letztes Format
formats = info.get("formats", [])
if formats:
return formats[-1].get("url")
except Exception as exc:
_log(f"yt-dlp Fehler fuer {video_id}: {exc}")
return None
def split_video_audio(url: str) -> tuple:
"""Trennt eine URL in (video_url, audio_url, meta_dict).
Falls kein Audio-Teil vorhanden: (url, None, {}).
meta_dict enthaelt Keys: vc, ac, w, h, fps, vbr, abr, asr, ach, dur
"""
if _AUDIO_SEP not in url:
return url, None, {}
parts = url.split(_AUDIO_SEP, 1)
video_url = parts[0]
rest = parts[1]
meta: Dict[str, str] = {}
audio_url = rest
if _META_SEP in rest:
audio_url, meta_str = rest.split(_META_SEP, 1)
for pair in meta_str.split(","):
if "=" in pair:
k, v = pair.split("=", 1)
meta[k] = v
return video_url, audio_url, meta

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.