Refine watchlist search and provider fallback

This commit is contained in:
Codex
2026-08-09 12:03:08 +02:00
parent 34e3625f87
commit bc7270d7c1
7 changed files with 158 additions and 80 deletions
+137 -14
View File
@@ -23,6 +23,10 @@ def clean_component(value, default="Anime"):
return text.strip(". ") or default
def normalize_title(value):
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
def parse_show_id(value, default_provider="anikoto"):
text = str(value or "").strip()
if ":" in text:
@@ -44,6 +48,52 @@ def bridge(command, provider, *args):
raise RuntimeError(f"Provider bridge returned invalid JSON: {exc}") from exc
def provider_order(primary):
ordered = []
primary = str(primary or "").strip().lower()
if primary in PROVIDERS:
ordered.append(primary)
for provider in PROVIDERS:
if provider not in ordered:
ordered.append(provider)
return ordered
def find_provider_show(provider, title, original_provider, original_show_id):
if provider == original_provider:
return original_show_id
query = str(title or "").strip()
if not query:
return ""
payload = bridge("search", provider, query, 1)
results = payload.get("results") or []
if not results:
return ""
target = normalize_title(query)
scored = []
for item in results:
candidate = normalize_title(item.get("title"))
if not candidate:
continue
if candidate == target:
score = 100
elif target and (candidate in target or target in candidate):
score = 75 - abs(len(candidate) - len(target))
else:
shared = set(target.split()) & set(candidate.split())
score = len(shared) * 5
scored.append((score, item))
if not scored:
return ""
scored.sort(key=lambda row: row[0], reverse=True)
if scored[0][0] <= 0:
return ""
provider_id = str(scored[0][1].get("provider_id") or scored[0][1].get("id") or "").strip()
if ":" in provider_id:
provider_id = provider_id.split(":", 1)[1].strip()
return provider_id
def episode_values(spec, episodes):
values = []
by_number = {str(ep.get("number")).rstrip("0").rstrip("."): ep for ep in episodes}
@@ -71,6 +121,47 @@ def episode_values(spec, episodes):
return values
def requested_episode_numbers(spec, episodes):
text = str(spec or "").strip()
if not text:
return [episode.get("number") for episode in episodes or [] if episode.get("number") not in (None, "")]
numbers = []
for part in re.split(r"[\s,]+", text):
if not part:
continue
if "-" in part:
start, end = part.split("-", 1)
try:
left = int(float(start))
right = int(float(end))
except ValueError:
continue
step = 1 if right >= left else -1
numbers.extend(range(left, right + step, step))
continue
if re.match(r"^\d+(?:\.\d+)?$", part):
numbers.append(float(part) if "." in part else int(part))
return numbers
def wanted_episodes(spec, episodes):
by_number = {str(ep.get("number")).rstrip("0").rstrip("."): ep for ep in episodes or []}
wanted = []
for number in requested_episode_numbers(spec, episodes):
key = str(number).rstrip("0").rstrip(".")
wanted.append(by_number.get(key) or {"number": number, "id": ""})
return wanted
def episode_by_number(number, episodes):
wanted = str(number).rstrip("0").rstrip(".")
for episode in episodes or []:
current = str(episode.get("number")).rstrip("0").rstrip(".")
if current == wanted:
return episode
return None
def ffmpeg_headers(headers):
pairs = []
for key, value in (headers or {}).items():
@@ -108,6 +199,24 @@ def download_episode(stream, target):
return subprocess.call(cmd)
def provider_episode_candidates(primary_provider, primary_show_id, title, wanted_episode):
number = wanted_episode.get("number")
for provider in provider_order(primary_provider):
try:
provider_show_id = find_provider_show(provider, title, primary_provider, primary_show_id)
if not provider_show_id:
print(f"Fallback skipped {provider}: no matching title.")
continue
episodes = bridge("episodes", provider, provider_show_id).get("episodes") or []
episode = episode_by_number(number, episodes)
if not episode:
print(f"Fallback skipped {provider}: episode {number} is not listed.")
continue
yield provider, provider_show_id, episode
except Exception as exc:
print(f"Fallback skipped {provider}: {exc}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--provider", default="anikoto")
@@ -122,7 +231,7 @@ def main():
provider, provider_show_id = parse_show_id(args.show_id, args.provider)
info = bridge("info", provider, provider_show_id)
episodes = bridge("episodes", provider, provider_show_id).get("episodes") or []
wanted = episode_values(args.episodes, episodes)
wanted = wanted_episodes(args.episodes, episodes)
if not wanted:
raise SystemExit(f"No matching episodes for spec {args.episodes!r}.")
@@ -140,20 +249,34 @@ def main():
padded = f"{int(float(number)):02d}" if str(number).replace(".", "", 1).isdigit() else str(number)
basename = f"{series_title} - S01E{padded}"
target = output_dir / f"{basename}.mp4"
print(f"Resolving episode {number} ({args.mode}, {args.quality})...")
stream = bridge("resolve", provider, ep_id, args.mode, args.quality)
print(f"Downloading episode {number} from {stream.get('quality') or 'auto'}...")
code = download_episode(stream, target)
if code != 0:
print(f"ffmpeg failed for episode {number} with exit code {code}", file=sys.stderr)
exit_code = code
last_error = None
downloaded = False
for active_provider, _active_show_id, active_ep in provider_episode_candidates(provider, provider_show_id, args.title or info.get("title"), ep):
try:
active_ep_id = active_ep.get("id")
print(f"Resolving episode {number} on {active_provider} ({args.mode}, {args.quality})...")
stream = bridge("resolve", active_provider, active_ep_id, args.mode, args.quality)
print(f"Downloading episode {number} from {active_provider} {stream.get('quality') or 'auto'}...")
code = download_episode(stream, target)
if code != 0:
last_error = RuntimeError(f"ffmpeg failed on {active_provider} with exit code {code}")
print(str(last_error), file=sys.stderr)
continue
try:
for subtitle_path in download_subtitles(stream.get("subtitles") or [], target):
print(f"Saved subtitle: {subtitle_path.name}")
except Exception as exc:
print(f"Subtitle download skipped: {exc}")
print(f"Saved: {target.name}")
downloaded = True
break
except Exception as exc:
last_error = exc
print(f"Provider {active_provider} failed for episode {number}: {exc}", file=sys.stderr)
if not downloaded:
print(f"Episode {number} failed on all providers: {last_error}", file=sys.stderr)
exit_code = 1
break
try:
for subtitle_path in download_subtitles(stream.get("subtitles") or [], target):
print(f"Saved subtitle: {subtitle_path.name}")
except Exception as exc:
print(f"Subtitle download skipped: {exc}")
print(f"Saved: {target.name}")
raise SystemExit(exit_code)