main: consolidate integrated changes after v0.1.54
This commit is contained in:
@@ -11,7 +11,7 @@ from dataclasses import dataclass
|
||||
import re
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.parse import urljoin
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeAlias
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
try: # pragma: no cover - optional dependency
|
||||
import requests
|
||||
@@ -33,8 +33,8 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
from requests import Session as RequestsSession
|
||||
from bs4 import BeautifulSoup as BeautifulSoupT # type: ignore[import-not-found]
|
||||
else: # pragma: no cover
|
||||
RequestsSession: TypeAlias = Any
|
||||
BeautifulSoupT: TypeAlias = Any
|
||||
RequestsSession = Any
|
||||
BeautifulSoupT = Any
|
||||
|
||||
|
||||
ADDON_ID = "plugin.video.viewit"
|
||||
@@ -53,6 +53,16 @@ SETTING_LOG_URLS = "log_urls_filmpalast"
|
||||
SETTING_DUMP_HTML = "dump_html_filmpalast"
|
||||
SETTING_SHOW_URL_INFO = "show_url_info_filmpalast"
|
||||
SETTING_LOG_ERRORS = "log_errors_filmpalast"
|
||||
ProgressCallback = Optional[Callable[[str, Optional[int]], Any]]
|
||||
|
||||
|
||||
def _emit_progress(callback: ProgressCallback, message: str, percent: Optional[int] = None) -> None:
|
||||
if not callable(callback):
|
||||
return
|
||||
try:
|
||||
callback(str(message or ""), None if percent is None else int(percent))
|
||||
except Exception:
|
||||
return
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
@@ -206,16 +216,26 @@ def _get_soup(url: str, *, session: Optional[RequestsSession] = None) -> Beautif
|
||||
raise RuntimeError("requests/bs4 sind nicht verfuegbar.")
|
||||
_log_visit(url)
|
||||
sess = session or get_requests_session("filmpalast", headers=HEADERS)
|
||||
response = None
|
||||
try:
|
||||
response = sess.get(url, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
_log_error_message(f"GET {url} failed: {exc}")
|
||||
raise
|
||||
if response.url and response.url != url:
|
||||
_log_url_event(response.url, kind="REDIRECT")
|
||||
_log_response_html(url, response.text)
|
||||
return BeautifulSoup(response.text, "html.parser")
|
||||
try:
|
||||
final_url = (response.url or url) if response is not None else url
|
||||
body = (response.text or "") if response is not None else ""
|
||||
if final_url != url:
|
||||
_log_url_event(final_url, kind="REDIRECT")
|
||||
_log_response_html(url, body)
|
||||
return BeautifulSoup(body, "html.parser")
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class FilmpalastPlugin(BasisPlugin):
|
||||
@@ -224,6 +244,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
|
||||
def __init__(self) -> None:
|
||||
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._hoster_cache: Dict[str, Dict[str, str]] = {}
|
||||
self._genre_to_url: Dict[str, str] = {}
|
||||
@@ -352,6 +373,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
seen_titles: set[str] = set()
|
||||
seen_urls: set[str] = set()
|
||||
for base_url, params in search_requests:
|
||||
response = None
|
||||
try:
|
||||
request_url = base_url if not params else f"{base_url}?{urlencode(params)}"
|
||||
_log_url_event(request_url, kind="GET")
|
||||
@@ -365,6 +387,12 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
except Exception as exc:
|
||||
_log_error_message(f"search request failed ({base_url}): {exc}")
|
||||
continue
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
anchors = soup.select("article.liste h2 a[href], article.liste h3 a[href]")
|
||||
if not anchors:
|
||||
@@ -466,9 +494,13 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
titles.sort(key=lambda value: value.casefold())
|
||||
return titles
|
||||
|
||||
async def search_titles(self, query: str) -> List[str]:
|
||||
async def search_titles(self, query: str, progress_callback: ProgressCallback = None) -> List[str]:
|
||||
_emit_progress(progress_callback, "Filmpalast Suche", 15)
|
||||
hits = self._search_hits(query)
|
||||
return self._apply_hits_to_title_index(hits)
|
||||
_emit_progress(progress_callback, f"Treffer verarbeiten ({len(hits)})", 70)
|
||||
titles = self._apply_hits_to_title_index(hits)
|
||||
_emit_progress(progress_callback, f"Fertig: {len(titles)} Treffer", 95)
|
||||
return titles
|
||||
|
||||
def _parse_genres(self, soup: BeautifulSoupT) -> Dict[str, str]:
|
||||
genres: Dict[str, str] = {}
|
||||
@@ -691,6 +723,64 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
return hit.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 "", ""
|
||||
root = soup.select_one("div#content[role='main']") or soup
|
||||
detail = root.select_one("article.detail") or root
|
||||
plot = ""
|
||||
poster = ""
|
||||
|
||||
# Filmpalast Detailseite: bevorzugt den dedizierten Filmhandlung-Block.
|
||||
plot_node = detail.select_one(
|
||||
"li[itemtype='http://schema.org/Movie'] span[itemprop='description']"
|
||||
)
|
||||
if plot_node is not None:
|
||||
plot = (plot_node.get_text(" ", strip=True) or "").strip()
|
||||
if not plot:
|
||||
hidden_plot = detail.select_one("cite span.hidden")
|
||||
if hidden_plot is not None:
|
||||
plot = (hidden_plot.get_text(" ", strip=True) or "").strip()
|
||||
if not plot:
|
||||
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||
node = root.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
plot = content
|
||||
break
|
||||
|
||||
# Filmpalast Detailseite: Cover liegt stabil in `img.cover2`.
|
||||
cover = detail.select_one("img.cover2")
|
||||
if cover is not None:
|
||||
value = (cover.get("data-src") or cover.get("src") or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
lower = candidate.casefold()
|
||||
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
|
||||
poster = candidate
|
||||
if not poster:
|
||||
thumb_node = detail.select_one("li[itemtype='http://schema.org/Movie'] img[itemprop='image']")
|
||||
if thumb_node is not None:
|
||||
value = (thumb_node.get("data-src") or thumb_node.get("src") or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
lower = candidate.casefold()
|
||||
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
|
||||
poster = candidate
|
||||
|
||||
return plot, poster
|
||||
|
||||
def remember_series_url(self, title: str, series_url: str) -> None:
|
||||
title = (title or "").strip()
|
||||
series_url = (series_url or "").strip()
|
||||
@@ -711,6 +801,52 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
return _series_hint_value(series_key)
|
||||
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:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
@@ -820,11 +956,23 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
|
||||
def available_hosters_for(self, title: str, season: str, episode: str) -> List[str]:
|
||||
detail_url = self._detail_url_for_selection(title, season, episode)
|
||||
hosters = self._hosters_for_detail_url(detail_url)
|
||||
return list(hosters.keys())
|
||||
return self.available_hosters_for_url(detail_url)
|
||||
|
||||
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
|
||||
detail_url = self._detail_url_for_selection(title, season, episode)
|
||||
return self.stream_link_for_url(detail_url)
|
||||
|
||||
def episode_url_for(self, title: str, season: str, episode: str) -> str:
|
||||
detail_url = self._detail_url_for_selection(title, season, episode)
|
||||
return (detail_url or "").strip()
|
||||
|
||||
def available_hosters_for_url(self, episode_url: str) -> List[str]:
|
||||
detail_url = (episode_url or "").strip()
|
||||
hosters = self._hosters_for_detail_url(detail_url)
|
||||
return list(hosters.keys())
|
||||
|
||||
def stream_link_for_url(self, episode_url: str) -> Optional[str]:
|
||||
detail_url = (episode_url or "").strip()
|
||||
if not detail_url:
|
||||
return None
|
||||
hosters = self._hosters_for_detail_url(detail_url)
|
||||
@@ -901,6 +1049,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
|
||||
redirected = link
|
||||
if self._requests_available:
|
||||
response = None
|
||||
try:
|
||||
session = get_requests_session("filmpalast", headers=HEADERS)
|
||||
response = session.get(link, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
|
||||
@@ -908,6 +1057,12 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
redirected = (response.url or link).strip() or link
|
||||
except Exception:
|
||||
redirected = link
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2) Danach optional die Redirect-URL nochmals auflösen.
|
||||
if callable(resolve_with_resolveurl) and redirected and redirected != link:
|
||||
@@ -922,3 +1077,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
_log_url_event(redirected, kind="FINAL")
|
||||
return redirected
|
||||
return None
|
||||
|
||||
|
||||
# Alias für die automatische Plugin-Erkennung.
|
||||
Plugin = FilmpalastPlugin
|
||||
|
||||
Reference in New Issue
Block a user