Nightly: refactor readability, progress callbacks, and resource handling
This commit is contained in:
@@ -13,7 +13,8 @@ import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
|
||||
from urllib.parse import quote
|
||||
|
||||
try: # pragma: no cover - optional dependency
|
||||
import requests
|
||||
@@ -69,6 +70,16 @@ HEADERS = {
|
||||
SESSION_CACHE_TTL_SECONDS = 300
|
||||
SESSION_CACHE_PREFIX = "viewit.aniworld"
|
||||
SESSION_CACHE_MAX_TITLE_URLS = 800
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -126,7 +137,7 @@ def _latest_episodes_url() -> str:
|
||||
|
||||
|
||||
def _search_url(query: str) -> str:
|
||||
return f"{_get_base_url()}/search?q={query}"
|
||||
return f"{_get_base_url()}/search?q={quote((query or '').strip())}"
|
||||
|
||||
|
||||
def _search_api_url() -> str:
|
||||
@@ -289,37 +300,56 @@ def _get_soup(url: str, *, session: Optional[RequestsSession] = None) -> Beautif
|
||||
_ensure_requests()
|
||||
_log_visit(url)
|
||||
sess = session or get_requests_session("aniworld", headers=HEADERS)
|
||||
response = None
|
||||
try:
|
||||
response = sess.get(url, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
_log_error(f"GET {url} failed: {exc}")
|
||||
raise
|
||||
if response.url and response.url != url:
|
||||
_log_url(response.url, kind="REDIRECT")
|
||||
_log_response_html(url, response.text)
|
||||
if _looks_like_cloudflare_challenge(response.text):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
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(final_url, kind="REDIRECT")
|
||||
_log_response_html(url, body)
|
||||
if _looks_like_cloudflare_challenge(body):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
return BeautifulSoup(body, "html.parser")
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _get_html_simple(url: str) -> str:
|
||||
_ensure_requests()
|
||||
_log_visit(url)
|
||||
sess = get_requests_session("aniworld", headers=HEADERS)
|
||||
response = None
|
||||
try:
|
||||
response = sess.get(url, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
_log_error(f"GET {url} failed: {exc}")
|
||||
raise
|
||||
if response.url and response.url != url:
|
||||
_log_url(response.url, kind="REDIRECT")
|
||||
body = response.text
|
||||
_log_response_html(url, body)
|
||||
if _looks_like_cloudflare_challenge(body):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
return body
|
||||
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(final_url, kind="REDIRECT")
|
||||
_log_response_html(url, body)
|
||||
if _looks_like_cloudflare_challenge(body):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
return body
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _get_soup_simple(url: str) -> BeautifulSoupT:
|
||||
@@ -351,17 +381,27 @@ def _post_json(url: str, *, payload: Dict[str, str], session: Optional[RequestsS
|
||||
_ensure_requests()
|
||||
_log_visit(url)
|
||||
sess = session or get_requests_session("aniworld", headers=HEADERS)
|
||||
response = sess.post(url, data=payload, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
if response.url and response.url != url:
|
||||
_log_url(response.url, kind="REDIRECT")
|
||||
_log_response_html(url, response.text)
|
||||
if _looks_like_cloudflare_challenge(response.text):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
response = None
|
||||
try:
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
response = sess.post(url, data=payload, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
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(final_url, kind="REDIRECT")
|
||||
_log_response_html(url, body)
|
||||
if _looks_like_cloudflare_challenge(body):
|
||||
raise RuntimeError("Cloudflare-Schutz erkannt. requests reicht ggf. nicht aus.")
|
||||
try:
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _extract_canonical_url(soup: BeautifulSoupT, fallback: str) -> str:
|
||||
@@ -555,10 +595,18 @@ def resolve_redirect(target_url: str) -> Optional[str]:
|
||||
_log_visit(normalized_url)
|
||||
session = get_requests_session("aniworld", headers=HEADERS)
|
||||
_get_soup(_get_base_url(), session=session)
|
||||
response = session.get(normalized_url, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
|
||||
if response.url:
|
||||
_log_url(response.url, kind="RESOLVED")
|
||||
return response.url if response.url else None
|
||||
response = None
|
||||
try:
|
||||
response = session.get(normalized_url, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
|
||||
if response.url:
|
||||
_log_url(response.url, kind="RESOLVED")
|
||||
return response.url if response.url else None
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def fetch_episode_hoster_names(episode_url: str) -> List[str]:
|
||||
@@ -629,11 +677,12 @@ def fetch_episode_stream_link(
|
||||
return resolved
|
||||
|
||||
|
||||
def search_animes(query: str) -> List[SeriesResult]:
|
||||
def search_animes(query: str, *, progress_callback: ProgressCallback = None) -> List[SeriesResult]:
|
||||
_ensure_requests()
|
||||
query = (query or "").strip()
|
||||
if not query:
|
||||
return []
|
||||
_emit_progress(progress_callback, "AniWorld API-Suche", 15)
|
||||
session = get_requests_session("aniworld", headers=HEADERS)
|
||||
try:
|
||||
session.get(_get_base_url(), headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
@@ -643,7 +692,9 @@ def search_animes(query: str) -> List[SeriesResult]:
|
||||
results: List[SeriesResult] = []
|
||||
seen: set[str] = set()
|
||||
if isinstance(data, list):
|
||||
for entry in data:
|
||||
for idx, entry in enumerate(data, start=1):
|
||||
if idx == 1 or idx % 50 == 0:
|
||||
_emit_progress(progress_callback, f"API auswerten {idx}/{len(data)}", 35)
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
title = _strip_html((entry.get("title") or "").strip())
|
||||
@@ -665,10 +716,16 @@ def search_animes(query: str) -> List[SeriesResult]:
|
||||
seen.add(key)
|
||||
description = (entry.get("description") or "").strip()
|
||||
results.append(SeriesResult(title=title, description=description, url=url))
|
||||
_emit_progress(progress_callback, f"API-Treffer: {len(results)}", 85)
|
||||
return results
|
||||
|
||||
soup = _get_soup_simple(_search_url(requests.utils.quote(query)))
|
||||
for anchor in soup.select("a[href^='/anime/stream/'][href]"):
|
||||
_emit_progress(progress_callback, "HTML-Suche (Fallback)", 55)
|
||||
soup = _get_soup_simple(_search_url(query))
|
||||
anchors = soup.select("a[href^='/anime/stream/'][href]")
|
||||
total_anchors = max(1, len(anchors))
|
||||
for idx, anchor in enumerate(anchors, start=1):
|
||||
if idx == 1 or idx % 100 == 0:
|
||||
_emit_progress(progress_callback, f"HTML auswerten {idx}/{total_anchors}", 70)
|
||||
href = (anchor.get("href") or "").strip()
|
||||
if not href or "/staffel-" in href or "/episode-" in href:
|
||||
continue
|
||||
@@ -686,6 +743,7 @@ def search_animes(query: str) -> List[SeriesResult]:
|
||||
continue
|
||||
seen.add(key)
|
||||
results.append(SeriesResult(title=title, description="", url=url))
|
||||
_emit_progress(progress_callback, f"HTML-Treffer: {len(results)}", 85)
|
||||
return results
|
||||
|
||||
|
||||
@@ -1151,7 +1209,7 @@ class AniworldPlugin(BasisPlugin):
|
||||
return self._episode_label_cache.get(cache_key, {}).get(episode_label)
|
||||
return None
|
||||
|
||||
async def search_titles(self, query: str) -> List[str]:
|
||||
async def search_titles(self, query: str, progress_callback: ProgressCallback = None) -> List[str]:
|
||||
query = (query or "").strip()
|
||||
if not query:
|
||||
self._anime_results.clear()
|
||||
@@ -1163,7 +1221,8 @@ class AniworldPlugin(BasisPlugin):
|
||||
if not self._requests_available:
|
||||
raise RuntimeError("AniworldPlugin kann ohne requests/bs4 nicht suchen.")
|
||||
try:
|
||||
results = search_animes(query)
|
||||
_emit_progress(progress_callback, "AniWorld Suche startet", 10)
|
||||
results = search_animes(query, progress_callback=progress_callback)
|
||||
except Exception as exc: # pragma: no cover
|
||||
self._anime_results.clear()
|
||||
self._season_cache.clear()
|
||||
@@ -1178,6 +1237,7 @@ class AniworldPlugin(BasisPlugin):
|
||||
self._season_cache.clear()
|
||||
self._season_links_cache.clear()
|
||||
self._episode_label_cache.clear()
|
||||
_emit_progress(progress_callback, f"Treffer aufbereitet: {len(results)}", 95)
|
||||
return [result.title for result in results]
|
||||
|
||||
def _ensure_seasons(self, title: str) -> List[SeasonInfo]:
|
||||
|
||||
Reference in New Issue
Block a user