Compare commits
1 Commits
nightly-20
...
d5a1125e03
| Author | SHA1 | Date | |
|---|---|---|---|
| d5a1125e03 |
204
addon/default.py
204
addon/default.py
@@ -1214,7 +1214,7 @@ def _show_plugin_search_results(plugin_name: str, query: str) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
results = [str(t).strip() for t in (results or []) if t and str(t).strip()]
|
results = _clean_search_titles([str(t).strip() for t in (results or []) if t and str(t).strip()])
|
||||||
results.sort(key=lambda value: value.casefold())
|
results.sort(key=lambda value: value.casefold())
|
||||||
|
|
||||||
use_source, show_tmdb, prefer_source = _metadata_policy(
|
use_source, show_tmdb, prefer_source = _metadata_policy(
|
||||||
@@ -1406,6 +1406,33 @@ def _series_url_params(plugin: BasisPlugin, title: str) -> dict[str, str]:
|
|||||||
return {"series_url": series_url} if series_url else {}
|
return {"series_url": series_url} if series_url else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_search_titles(values: list[str]) -> list[str]:
|
||||||
|
"""Filtert offensichtliche Platzhalter und dedupliziert Treffer."""
|
||||||
|
blocked = {
|
||||||
|
"stream",
|
||||||
|
"streams",
|
||||||
|
"film",
|
||||||
|
"movie",
|
||||||
|
"play",
|
||||||
|
"details",
|
||||||
|
"details/play",
|
||||||
|
}
|
||||||
|
cleaned: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in values:
|
||||||
|
title = (raw or "").strip()
|
||||||
|
if not title:
|
||||||
|
continue
|
||||||
|
key = title.casefold()
|
||||||
|
if key in blocked:
|
||||||
|
continue
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
cleaned.append(title)
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
def _show_search() -> None:
|
def _show_search() -> None:
|
||||||
_log("Suche gestartet.")
|
_log("Suche gestartet.")
|
||||||
dialog = xbmcgui.Dialog()
|
dialog = xbmcgui.Dialog()
|
||||||
@@ -1453,7 +1480,7 @@ def _show_search_results(query: str) -> None:
|
|||||||
pass
|
pass
|
||||||
_log(f"Suche fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
_log(f"Suche fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||||
continue
|
continue
|
||||||
results = [str(t).strip() for t in (results or []) if t and str(t).strip()]
|
results = _clean_search_titles([str(t).strip() for t in (results or []) if t and str(t).strip()])
|
||||||
_log(f"Treffer ({plugin_name}): {len(results)}", xbmc.LOGDEBUG)
|
_log(f"Treffer ({plugin_name}): {len(results)}", xbmc.LOGDEBUG)
|
||||||
use_source, show_tmdb, prefer_source = _metadata_policy(
|
use_source, show_tmdb, prefer_source = _metadata_policy(
|
||||||
plugin_name, plugin, allow_tmdb=_tmdb_enabled()
|
plugin_name, plugin, allow_tmdb=_tmdb_enabled()
|
||||||
@@ -1537,6 +1564,73 @@ def _show_search_results(query: str) -> None:
|
|||||||
xbmcplugin.endOfDirectory(handle)
|
xbmcplugin.endOfDirectory(handle)
|
||||||
|
|
||||||
|
|
||||||
|
def _movie_seed_for_title(plugin: BasisPlugin, title: str, seasons: list[str]) -> tuple[str, str] | None:
|
||||||
|
"""Ermittelt ein Film-Seed (Season/Episode), um direkt Provider anzeigen zu können."""
|
||||||
|
if not seasons or len(seasons) != 1:
|
||||||
|
return None
|
||||||
|
season = str(seasons[0] or "").strip()
|
||||||
|
if not season:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
episodes = [str(value or "").strip() for value in (plugin.episodes_for(title, season) or [])]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
episodes = [value for value in episodes if value]
|
||||||
|
if len(episodes) != 1:
|
||||||
|
return None
|
||||||
|
episode = episodes[0]
|
||||||
|
season_key = season.casefold()
|
||||||
|
episode_key = episode.casefold()
|
||||||
|
title_key = (title or "").strip().casefold()
|
||||||
|
generic_seasons = {"film", "movie", "stream"}
|
||||||
|
generic_episodes = {"stream", "film", "play", title_key}
|
||||||
|
if season_key in generic_seasons and episode_key in generic_episodes:
|
||||||
|
return (season, episode)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _show_movie_streams(
|
||||||
|
plugin_name: str,
|
||||||
|
title: str,
|
||||||
|
season: str,
|
||||||
|
episode: str,
|
||||||
|
*,
|
||||||
|
series_url: str = "",
|
||||||
|
) -> None:
|
||||||
|
handle = _get_handle()
|
||||||
|
plugin = _discover_plugins().get(plugin_name)
|
||||||
|
if plugin is None:
|
||||||
|
xbmcgui.Dialog().notification("Streams", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||||
|
xbmcplugin.endOfDirectory(handle)
|
||||||
|
return
|
||||||
|
|
||||||
|
if series_url:
|
||||||
|
remember_series_url = getattr(plugin, "remember_series_url", None)
|
||||||
|
if callable(remember_series_url):
|
||||||
|
try:
|
||||||
|
remember_series_url(title, series_url)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
xbmcplugin.setPluginCategory(handle, f"{title} - Streams")
|
||||||
|
_set_content(handle, "videos")
|
||||||
|
|
||||||
|
base_params = {"plugin": plugin_name, "title": title, "season": season, "episode": episode}
|
||||||
|
if series_url:
|
||||||
|
base_params["series_url"] = series_url
|
||||||
|
|
||||||
|
# Hoster bleiben im Auswahldialog der Wiedergabe (wie bisher).
|
||||||
|
_add_directory_item(
|
||||||
|
handle,
|
||||||
|
title,
|
||||||
|
"play_episode",
|
||||||
|
dict(base_params),
|
||||||
|
is_folder=False,
|
||||||
|
info_labels={"title": title, "mediatype": "movie"},
|
||||||
|
)
|
||||||
|
xbmcplugin.endOfDirectory(handle)
|
||||||
|
|
||||||
|
|
||||||
def _show_seasons(plugin_name: str, title: str, series_url: str = "") -> None:
|
def _show_seasons(plugin_name: str, title: str, series_url: str = "") -> None:
|
||||||
handle = _get_handle()
|
handle = _get_handle()
|
||||||
_log(f"Staffeln laden: {plugin_name} / {title}")
|
_log(f"Staffeln laden: {plugin_name} / {title}")
|
||||||
@@ -1553,60 +1647,6 @@ def _show_seasons(plugin_name: str, title: str, series_url: str = "") -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Einschalten liefert Filme. Für Playback soll nach dem Öffnen des Titels direkt ein
|
|
||||||
# einzelnes abspielbares Item angezeigt werden: <Titel> -> (<Titel> abspielbar).
|
|
||||||
# Wichtig: ohne zusätzliche Netzwerkanfragen (sonst bleibt Kodi ggf. im Busy-Spinner hängen).
|
|
||||||
if (plugin_name or "").casefold() == "einschalten" and _get_setting_bool("einschalten_enable_playback", default=False):
|
|
||||||
xbmcplugin.setPluginCategory(handle, title)
|
|
||||||
_set_content(handle, "movies")
|
|
||||||
playstate = _title_playstate(plugin_name, title)
|
|
||||||
info_labels: dict[str, object] = {"title": title, "mediatype": "movie"}
|
|
||||||
info_labels = _apply_playstate_to_info(info_labels, playstate)
|
|
||||||
display_label = _label_with_playstate(title, playstate)
|
|
||||||
movie_params = {"plugin": plugin_name, "title": title}
|
|
||||||
if series_url:
|
|
||||||
movie_params["series_url"] = series_url
|
|
||||||
_add_directory_item(
|
|
||||||
handle,
|
|
||||||
display_label,
|
|
||||||
"play_movie",
|
|
||||||
movie_params,
|
|
||||||
is_folder=False,
|
|
||||||
info_labels=info_labels,
|
|
||||||
)
|
|
||||||
xbmcplugin.endOfDirectory(handle)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Optional: Plugins können schnell (ohne Detail-Request) sagen, ob ein Titel ein Film ist.
|
|
||||||
# Dann zeigen wir direkt ein einzelnes abspielbares Item: <Titel> -> (<Titel>).
|
|
||||||
is_movie = getattr(plugin, "is_movie", None)
|
|
||||||
if callable(is_movie):
|
|
||||||
try:
|
|
||||||
if bool(is_movie(title)):
|
|
||||||
xbmcplugin.setPluginCategory(handle, title)
|
|
||||||
_set_content(handle, "movies")
|
|
||||||
playstate = _title_playstate(plugin_name, title)
|
|
||||||
info_labels: dict[str, object] = {"title": title, "mediatype": "movie"}
|
|
||||||
info_labels = _apply_playstate_to_info(info_labels, playstate)
|
|
||||||
display_label = _label_with_playstate(title, playstate)
|
|
||||||
movie_params = {"plugin": plugin_name, "title": title}
|
|
||||||
if series_url:
|
|
||||||
movie_params["series_url"] = series_url
|
|
||||||
else:
|
|
||||||
movie_params.update(_series_url_params(plugin, title))
|
|
||||||
_add_directory_item(
|
|
||||||
handle,
|
|
||||||
display_label,
|
|
||||||
"play_movie",
|
|
||||||
movie_params,
|
|
||||||
is_folder=False,
|
|
||||||
info_labels=info_labels,
|
|
||||||
)
|
|
||||||
xbmcplugin.endOfDirectory(handle)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
use_source, show_tmdb, _prefer_source = _metadata_policy(
|
use_source, show_tmdb, _prefer_source = _metadata_policy(
|
||||||
plugin_name, plugin, allow_tmdb=_tmdb_enabled()
|
plugin_name, plugin, allow_tmdb=_tmdb_enabled()
|
||||||
)
|
)
|
||||||
@@ -1636,6 +1676,26 @@ def _show_seasons(plugin_name: str, title: str, series_url: str = "") -> None:
|
|||||||
xbmcplugin.endOfDirectory(handle)
|
xbmcplugin.endOfDirectory(handle)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
movie_seed = _movie_seed_for_title(plugin, title, seasons)
|
||||||
|
if movie_seed is not None:
|
||||||
|
# Dieser Action-Pfad wurde als Verzeichnis aufgerufen. Ohne endOfDirectory()
|
||||||
|
# bleibt Kodi im Busy-Zustand, auch wenn wir direkt in die Wiedergabe springen.
|
||||||
|
try:
|
||||||
|
xbmcplugin.endOfDirectory(handle, succeeded=False)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
xbmcplugin.endOfDirectory(handle)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_play_episode(
|
||||||
|
plugin_name,
|
||||||
|
title,
|
||||||
|
movie_seed[0],
|
||||||
|
movie_seed[1],
|
||||||
|
series_url=series_url,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
count = len(seasons)
|
count = len(seasons)
|
||||||
suffix = "Staffel" if count == 1 else "Staffeln"
|
suffix = "Staffel" if count == 1 else "Staffeln"
|
||||||
xbmcplugin.setPluginCategory(handle, f"{title} ({count} {suffix})")
|
xbmcplugin.setPluginCategory(handle, f"{title} ({count} {suffix})")
|
||||||
@@ -3210,12 +3270,29 @@ def _track_playback_and_update_state(key: str) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _track_playback_and_update_state_async(key: str) -> None:
|
||||||
|
"""Startet Playstate-Tracking im Hintergrund, damit die UI nicht blockiert."""
|
||||||
|
key = (key or "").strip()
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _worker() -> None:
|
||||||
|
try:
|
||||||
|
_track_playback_and_update_state(key)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
worker = threading.Thread(target=_worker, name="viewit-playstate-tracker", daemon=True)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
|
||||||
def _play_episode(
|
def _play_episode(
|
||||||
plugin_name: str,
|
plugin_name: str,
|
||||||
title: str,
|
title: str,
|
||||||
season: str,
|
season: str,
|
||||||
episode: str,
|
episode: str,
|
||||||
*,
|
*,
|
||||||
|
forced_hoster: str = "",
|
||||||
episode_url: str = "",
|
episode_url: str = "",
|
||||||
series_url: str = "",
|
series_url: str = "",
|
||||||
resolve_handle: int | None = None,
|
resolve_handle: int | None = None,
|
||||||
@@ -3260,10 +3337,16 @@ def _play_episode(
|
|||||||
_log(f"Hoster laden fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
_log(f"Hoster laden fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||||
|
|
||||||
selected_hoster: str | None = None
|
selected_hoster: str | None = None
|
||||||
|
forced_hoster = (forced_hoster or "").strip()
|
||||||
if available_hosters:
|
if available_hosters:
|
||||||
if len(available_hosters) == 1:
|
if forced_hoster:
|
||||||
|
for hoster in available_hosters:
|
||||||
|
if hoster.casefold() == forced_hoster.casefold():
|
||||||
|
selected_hoster = hoster
|
||||||
|
break
|
||||||
|
if selected_hoster is None and len(available_hosters) == 1:
|
||||||
selected_hoster = available_hosters[0]
|
selected_hoster = available_hosters[0]
|
||||||
else:
|
elif selected_hoster is None:
|
||||||
selected_index = xbmcgui.Dialog().select("Hoster waehlen", available_hosters)
|
selected_index = xbmcgui.Dialog().select("Hoster waehlen", available_hosters)
|
||||||
if selected_index is None or selected_index < 0:
|
if selected_index is None or selected_index < 0:
|
||||||
_log("Play abgebrochen (kein Hoster gewählt).", xbmc.LOGDEBUG)
|
_log("Play abgebrochen (kein Hoster gewählt).", xbmc.LOGDEBUG)
|
||||||
@@ -3308,7 +3391,7 @@ def _play_episode(
|
|||||||
cast=cast,
|
cast=cast,
|
||||||
resolve_handle=resolve_handle,
|
resolve_handle=resolve_handle,
|
||||||
)
|
)
|
||||||
_track_playback_and_update_state(
|
_track_playback_and_update_state_async(
|
||||||
_playstate_key(plugin_name=plugin_name, title=title, season=season, episode=episode)
|
_playstate_key(plugin_name=plugin_name, title=title, season=season, episode=episode)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3396,7 +3479,7 @@ def _play_episode_url(
|
|||||||
cast=cast,
|
cast=cast,
|
||||||
resolve_handle=resolve_handle,
|
resolve_handle=resolve_handle,
|
||||||
)
|
)
|
||||||
_track_playback_and_update_state(
|
_track_playback_and_update_state_async(
|
||||||
_playstate_key(plugin_name=plugin_name, title=title, season=season_label, episode=episode_label)
|
_playstate_key(plugin_name=plugin_name, title=title, season=season_label, episode=episode_label)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3496,6 +3579,7 @@ def run() -> None:
|
|||||||
params.get("title", ""),
|
params.get("title", ""),
|
||||||
params.get("season", ""),
|
params.get("season", ""),
|
||||||
params.get("episode", ""),
|
params.get("episode", ""),
|
||||||
|
forced_hoster=params.get("hoster", ""),
|
||||||
episode_url=params.get("url", ""),
|
episode_url=params.get("url", ""),
|
||||||
series_url=params.get("series_url", ""),
|
series_url=params.get("series_url", ""),
|
||||||
resolve_handle=_get_handle(),
|
resolve_handle=_get_handle(),
|
||||||
|
|||||||
@@ -754,6 +754,7 @@ class AniworldPlugin(BasisPlugin):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._anime_results: Dict[str, SeriesResult] = {}
|
self._anime_results: Dict[str, SeriesResult] = {}
|
||||||
self._title_url_cache: Dict[str, str] = self._load_title_url_cache()
|
self._title_url_cache: Dict[str, str] = self._load_title_url_cache()
|
||||||
|
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||||
self._genre_names_cache: Optional[List[str]] = None
|
self._genre_names_cache: Optional[List[str]] = None
|
||||||
self._season_cache: Dict[str, List[SeasonInfo]] = {}
|
self._season_cache: Dict[str, List[SeasonInfo]] = {}
|
||||||
self._season_links_cache: Dict[str, List[SeasonInfo]] = {}
|
self._season_links_cache: Dict[str, List[SeasonInfo]] = {}
|
||||||
@@ -818,8 +819,64 @@ class AniworldPlugin(BasisPlugin):
|
|||||||
changed = True
|
changed = True
|
||||||
if changed and persist:
|
if changed and persist:
|
||||||
self._save_title_url_cache()
|
self._save_title_url_cache()
|
||||||
|
if description:
|
||||||
|
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
self._title_meta[title] = (description.strip() or old_plot, old_poster)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
merged_plot = (plot or old_plot or "").strip()
|
||||||
|
merged_poster = (poster or old_poster or "").strip()
|
||||||
|
self._title_meta[title] = (merged_plot, merged_poster)
|
||||||
|
|
||||||
|
def _extract_series_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||||
|
if not soup:
|
||||||
|
return "", ""
|
||||||
|
plot = ""
|
||||||
|
poster = ""
|
||||||
|
|
||||||
|
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
plot = content
|
||||||
|
break
|
||||||
|
if not plot:
|
||||||
|
for selector in (".series-description", ".seri_des", ".description", "article p"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
text = (node.get_text(" ", strip=True) or "").strip()
|
||||||
|
if text:
|
||||||
|
plot = text
|
||||||
|
break
|
||||||
|
|
||||||
|
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
poster = _absolute_url(content)
|
||||||
|
break
|
||||||
|
if not poster:
|
||||||
|
for selector in ("img.seriesCoverBox", ".seriesCoverBox img", "img[alt][src]"):
|
||||||
|
image = soup.select_one(selector)
|
||||||
|
if image is None:
|
||||||
|
continue
|
||||||
|
value = (image.get("data-src") or image.get("src") or "").strip()
|
||||||
|
if value:
|
||||||
|
poster = _absolute_url(value)
|
||||||
|
break
|
||||||
|
|
||||||
|
return plot, poster
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _season_links_cache_name(series_url: str) -> str:
|
def _season_links_cache_name(series_url: str) -> str:
|
||||||
digest = hashlib.sha1((series_url or "").encode("utf-8")).hexdigest()[:20]
|
digest = hashlib.sha1((series_url or "").encode("utf-8")).hexdigest()[:20]
|
||||||
@@ -951,6 +1008,40 @@ class AniworldPlugin(BasisPlugin):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return {}, {}, None
|
||||||
|
|
||||||
|
info: dict[str, str] = {"title": title}
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
if cached_plot:
|
||||||
|
info["plot"] = cached_plot
|
||||||
|
if cached_poster:
|
||||||
|
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||||
|
if "plot" in info and art:
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
|
series = self._find_series_by_title(title)
|
||||||
|
if series is None or not series.url:
|
||||||
|
return info, art, None
|
||||||
|
if series.description and "plot" not in info:
|
||||||
|
info["plot"] = series.description
|
||||||
|
|
||||||
|
try:
|
||||||
|
soup = _get_soup(series.url, session=get_requests_session("aniworld", headers=HEADERS))
|
||||||
|
plot, poster = self._extract_series_metadata(soup)
|
||||||
|
except Exception:
|
||||||
|
plot, poster = "", ""
|
||||||
|
|
||||||
|
if plot:
|
||||||
|
info["plot"] = plot
|
||||||
|
if poster:
|
||||||
|
art = {"thumb": poster, "poster": poster}
|
||||||
|
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
def _ensure_popular(self) -> List[SeriesResult]:
|
def _ensure_popular(self) -> List[SeriesResult]:
|
||||||
if self._popular_cache is not None:
|
if self._popular_cache is not None:
|
||||||
return list(self._popular_cache)
|
return list(self._popular_cache)
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ class FilmpalastPlugin(BasisPlugin):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._title_to_url: Dict[str, str] = {}
|
self._title_to_url: Dict[str, str] = {}
|
||||||
|
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||||
self._series_entries: Dict[str, Dict[int, Dict[int, EpisodeEntry]]] = {}
|
self._series_entries: Dict[str, Dict[int, Dict[int, EpisodeEntry]]] = {}
|
||||||
self._hoster_cache: Dict[str, Dict[str, str]] = {}
|
self._hoster_cache: Dict[str, Dict[str, str]] = {}
|
||||||
self._genre_to_url: Dict[str, str] = {}
|
self._genre_to_url: Dict[str, str] = {}
|
||||||
@@ -722,6 +723,59 @@ class FilmpalastPlugin(BasisPlugin):
|
|||||||
return hit.url
|
return hit.url
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
merged_plot = (plot or old_plot or "").strip()
|
||||||
|
merged_poster = (poster or old_poster or "").strip()
|
||||||
|
self._title_meta[title] = (merged_plot, merged_poster)
|
||||||
|
|
||||||
|
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||||
|
if not soup:
|
||||||
|
return "", ""
|
||||||
|
plot = ""
|
||||||
|
poster = ""
|
||||||
|
|
||||||
|
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
plot = content
|
||||||
|
break
|
||||||
|
if not plot:
|
||||||
|
for selector in (".toggle-content .coverDetails", ".entry-content p", "article p"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
text = (node.get_text(" ", strip=True) or "").strip()
|
||||||
|
if text and len(text) > 40:
|
||||||
|
plot = text
|
||||||
|
break
|
||||||
|
|
||||||
|
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
poster = _absolute_url(content)
|
||||||
|
break
|
||||||
|
if not poster:
|
||||||
|
for selector in ("img.cover", "article img", ".entry-content img"):
|
||||||
|
image = soup.select_one(selector)
|
||||||
|
if image is None:
|
||||||
|
continue
|
||||||
|
value = (image.get("data-src") or image.get("src") or "").strip()
|
||||||
|
if value:
|
||||||
|
poster = _absolute_url(value)
|
||||||
|
break
|
||||||
|
|
||||||
|
return plot, poster
|
||||||
|
|
||||||
def remember_series_url(self, title: str, series_url: str) -> None:
|
def remember_series_url(self, title: str, series_url: str) -> None:
|
||||||
title = (title or "").strip()
|
title = (title or "").strip()
|
||||||
series_url = (series_url or "").strip()
|
series_url = (series_url or "").strip()
|
||||||
@@ -742,6 +796,52 @@ class FilmpalastPlugin(BasisPlugin):
|
|||||||
return _series_hint_value(series_key)
|
return _series_hint_value(series_key)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return {}, {}, None
|
||||||
|
|
||||||
|
info: dict[str, str] = {"title": title}
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
if cached_plot:
|
||||||
|
info["plot"] = cached_plot
|
||||||
|
if cached_poster:
|
||||||
|
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||||
|
if "plot" in info and art:
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
|
detail_url = self._ensure_title_url(title)
|
||||||
|
if not detail_url:
|
||||||
|
series_key = self._series_key_for_title(title) or self._ensure_series_entries_for_title(title)
|
||||||
|
if series_key:
|
||||||
|
seasons = self._series_entries.get(series_key, {})
|
||||||
|
first_entry: Optional[EpisodeEntry] = None
|
||||||
|
for season_number in sorted(seasons.keys()):
|
||||||
|
episodes = seasons.get(season_number, {})
|
||||||
|
for episode_number in sorted(episodes.keys()):
|
||||||
|
first_entry = episodes.get(episode_number)
|
||||||
|
if first_entry is not None:
|
||||||
|
break
|
||||||
|
if first_entry is not None:
|
||||||
|
break
|
||||||
|
detail_url = first_entry.url if first_entry is not None else ""
|
||||||
|
if not detail_url:
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
soup = _get_soup(detail_url, session=get_requests_session("filmpalast", headers=HEADERS))
|
||||||
|
plot, poster = self._extract_detail_metadata(soup)
|
||||||
|
except Exception:
|
||||||
|
plot, poster = "", ""
|
||||||
|
|
||||||
|
if plot:
|
||||||
|
info["plot"] = plot
|
||||||
|
if poster:
|
||||||
|
art = {"thumb": poster, "poster": poster}
|
||||||
|
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
def is_movie(self, title: str) -> bool:
|
def is_movie(self, title: str) -> bool:
|
||||||
title = (title or "").strip()
|
title = (title or "").strip()
|
||||||
if not title:
|
if not title:
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class SearchHit:
|
|||||||
title: str
|
title: str
|
||||||
url: str
|
url: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
poster: str = ""
|
||||||
|
|
||||||
|
|
||||||
def _normalize_search_text(value: str) -> str:
|
def _normalize_search_text(value: str) -> str:
|
||||||
@@ -149,6 +150,7 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
self._season_to_episode_numbers: Dict[tuple[str, str], List[int]] = {}
|
self._season_to_episode_numbers: Dict[tuple[str, str], List[int]] = {}
|
||||||
self._episode_title_by_number: Dict[tuple[str, int, int], str] = {}
|
self._episode_title_by_number: Dict[tuple[str, int, int], str] = {}
|
||||||
self._detail_html_cache: Dict[str, str] = {}
|
self._detail_html_cache: Dict[str, str] = {}
|
||||||
|
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||||
self._popular_cache: List[str] | None = None
|
self._popular_cache: List[str] | None = None
|
||||||
self._default_preferred_hosters: List[str] = list(DEFAULT_PREFERRED_HOSTERS)
|
self._default_preferred_hosters: List[str] = list(DEFAULT_PREFERRED_HOSTERS)
|
||||||
self._preferred_hosters: List[str] = list(self._default_preferred_hosters)
|
self._preferred_hosters: List[str] = list(self._default_preferred_hosters)
|
||||||
@@ -429,6 +431,7 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
continue
|
continue
|
||||||
seen.add(hit.title)
|
seen.add(hit.title)
|
||||||
self._title_to_url[hit.title] = hit.url
|
self._title_to_url[hit.title] = hit.url
|
||||||
|
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||||
titles.append(hit.title)
|
titles.append(hit.title)
|
||||||
if titles:
|
if titles:
|
||||||
self._save_title_url_cache()
|
self._save_title_url_cache()
|
||||||
@@ -487,6 +490,69 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def _pick_image_from_node(self, node: Any) -> str:
|
||||||
|
if node is None:
|
||||||
|
return ""
|
||||||
|
image = node.select_one("img")
|
||||||
|
if image is None:
|
||||||
|
return ""
|
||||||
|
for attr in ("data-src", "src"):
|
||||||
|
value = (image.get(attr) or "").strip()
|
||||||
|
if value and "lazy_placeholder" not in value.casefold():
|
||||||
|
return self._absolute_external_url(value, base=self._get_base_url())
|
||||||
|
srcset = (image.get("data-srcset") or image.get("srcset") or "").strip()
|
||||||
|
if srcset:
|
||||||
|
first = srcset.split(",")[0].strip().split(" ", 1)[0].strip()
|
||||||
|
if first:
|
||||||
|
return self._absolute_external_url(first, base=self._get_base_url())
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
merged_plot = (plot or old_plot or "").strip()
|
||||||
|
merged_poster = (poster or old_poster or "").strip()
|
||||||
|
self._title_meta[title] = (merged_plot, merged_poster)
|
||||||
|
|
||||||
|
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||||
|
if not soup:
|
||||||
|
return "", ""
|
||||||
|
plot = ""
|
||||||
|
poster = ""
|
||||||
|
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
plot = content
|
||||||
|
break
|
||||||
|
if not plot:
|
||||||
|
candidates: list[str] = []
|
||||||
|
for paragraph in soup.select("article p, .TPost p, .Description p, .entry-content p"):
|
||||||
|
text = (paragraph.get_text(" ", strip=True) or "").strip()
|
||||||
|
if len(text) >= 60:
|
||||||
|
candidates.append(text)
|
||||||
|
if candidates:
|
||||||
|
plot = max(candidates, key=len)
|
||||||
|
|
||||||
|
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
|
||||||
|
node = soup.select_one(selector)
|
||||||
|
if node is None:
|
||||||
|
continue
|
||||||
|
content = (node.get("content") or "").strip()
|
||||||
|
if content:
|
||||||
|
poster = self._absolute_external_url(content, base=self._get_base_url())
|
||||||
|
break
|
||||||
|
if not poster:
|
||||||
|
for selector in ("article", ".TPost", ".entry-content"):
|
||||||
|
poster = self._pick_image_from_node(soup.select_one(selector))
|
||||||
|
if poster:
|
||||||
|
break
|
||||||
|
return plot, poster
|
||||||
|
|
||||||
def _clear_stream_index_for_title(self, title: str) -> None:
|
def _clear_stream_index_for_title(self, title: str) -> None:
|
||||||
for key in list(self._season_to_episode_numbers.keys()):
|
for key in list(self._season_to_episode_numbers.keys()):
|
||||||
if key[0] == title:
|
if key[0] == title:
|
||||||
@@ -721,7 +787,17 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
continue
|
continue
|
||||||
if is_movie_hint:
|
if is_movie_hint:
|
||||||
self._movie_title_hint.add(title)
|
self._movie_title_hint.add(title)
|
||||||
hits.append(SearchHit(title=title, url=self._absolute_url(href), description=""))
|
description_tag = item.select_one(".TPMvCn .Description, .Description, .entry-summary")
|
||||||
|
description = (description_tag.get_text(" ", strip=True) or "").strip() if description_tag else ""
|
||||||
|
poster = self._pick_image_from_node(item)
|
||||||
|
hits.append(
|
||||||
|
SearchHit(
|
||||||
|
title=title,
|
||||||
|
url=self._absolute_url(href),
|
||||||
|
description=description,
|
||||||
|
poster=poster,
|
||||||
|
)
|
||||||
|
)
|
||||||
return hits
|
return hits
|
||||||
|
|
||||||
def is_movie(self, title: str) -> bool:
|
def is_movie(self, title: str) -> bool:
|
||||||
@@ -794,6 +870,7 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
continue
|
continue
|
||||||
seen.add(hit.title)
|
seen.add(hit.title)
|
||||||
self._title_to_url[hit.title] = hit.url
|
self._title_to_url[hit.title] = hit.url
|
||||||
|
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||||
titles.append(hit.title)
|
titles.append(hit.title)
|
||||||
if titles:
|
if titles:
|
||||||
self._save_title_url_cache()
|
self._save_title_url_cache()
|
||||||
@@ -905,7 +982,8 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
self._movie_title_hint.add(title)
|
self._movie_title_hint.add(title)
|
||||||
description_tag = item.select_one(".TPMvCn .Description")
|
description_tag = item.select_one(".TPMvCn .Description")
|
||||||
description = description_tag.get_text(" ", strip=True) if description_tag else ""
|
description = description_tag.get_text(" ", strip=True) if description_tag else ""
|
||||||
hit = SearchHit(title=title, url=self._absolute_url(href), description=description)
|
poster = self._pick_image_from_node(item)
|
||||||
|
hit = SearchHit(title=title, url=self._absolute_url(href), description=description, poster=poster)
|
||||||
if _matches_query(query, title=hit.title, description=hit.description):
|
if _matches_query(query, title=hit.title, description=hit.description):
|
||||||
hits.append(hit)
|
hits.append(hit)
|
||||||
|
|
||||||
@@ -918,11 +996,41 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
continue
|
continue
|
||||||
seen.add(hit.title)
|
seen.add(hit.title)
|
||||||
self._title_to_url[hit.title] = hit.url
|
self._title_to_url[hit.title] = hit.url
|
||||||
|
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||||
titles.append(hit.title)
|
titles.append(hit.title)
|
||||||
self._save_title_url_cache()
|
self._save_title_url_cache()
|
||||||
_emit_progress(progress_callback, f"Fertig: {len(titles)} Treffer", 95)
|
_emit_progress(progress_callback, f"Fertig: {len(titles)} Treffer", 95)
|
||||||
return titles
|
return titles
|
||||||
|
|
||||||
|
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||||
|
title = (title or "").strip()
|
||||||
|
if not title:
|
||||||
|
return {}, {}, None
|
||||||
|
|
||||||
|
info: dict[str, str] = {"title": title}
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
|
||||||
|
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||||
|
if cached_plot:
|
||||||
|
info["plot"] = cached_plot
|
||||||
|
if cached_poster:
|
||||||
|
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||||
|
|
||||||
|
if "plot" in info and art:
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
|
soup = self._get_detail_soup(title)
|
||||||
|
if soup is None:
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
|
plot, poster = self._extract_detail_metadata(soup)
|
||||||
|
if plot:
|
||||||
|
info["plot"] = plot
|
||||||
|
if poster:
|
||||||
|
art = {"thumb": poster, "poster": poster}
|
||||||
|
self._store_title_meta(title, plot=plot, poster=poster)
|
||||||
|
return info, art, None
|
||||||
|
|
||||||
def genres(self) -> List[str]:
|
def genres(self) -> List[str]:
|
||||||
if not REQUESTS_AVAILABLE or BeautifulSoup is None:
|
if not REQUESTS_AVAILABLE or BeautifulSoup is None:
|
||||||
return []
|
return []
|
||||||
|
|||||||
Reference in New Issue
Block a user