Fix provider HLS downloads

This commit is contained in:
Codex
2026-08-09 12:32:41 +02:00
parent cb86f79da1
commit 54d559d73c
5 changed files with 175 additions and 17 deletions
+130 -15
View File
@@ -8,6 +8,7 @@ import os
import re
import subprocess
import sys
from urllib.parse import urljoin
import urllib.request
from pathlib import Path
@@ -37,6 +38,19 @@ def parse_show_id(value, default_provider="anikoto"):
return default_provider, text
def episode_key(value):
text = str(value or "").strip()
if not text:
return ""
try:
number = float(text)
except ValueError:
return text
if number.is_integer():
return str(int(number))
return str(number).rstrip("0").rstrip(".")
def bridge(command, provider, *args):
cmd = ["node", str(BRIDGE), command, provider, *[str(arg) for arg in args]]
proc = subprocess.run(cmd, cwd=PROJECT_ROOT, text=True, capture_output=True, check=False)
@@ -96,7 +110,7 @@ def find_provider_show(provider, title, original_provider, original_show_id):
def episode_values(spec, episodes):
values = []
by_number = {str(ep.get("number")).rstrip("0").rstrip("."): ep for ep in episodes}
by_number = {episode_key(ep.get("number")): ep for ep in episodes}
if not str(spec or "").strip():
return episodes
for part in re.split(r"[\s,]+", str(spec).strip()):
@@ -115,7 +129,7 @@ def episode_values(spec, episodes):
if item and item not in values:
values.append(item)
continue
item = by_number.get(str(float(part)).rstrip("0").rstrip(".")) if re.match(r"^\d+(?:\.\d+)?$", part) else None
item = by_number.get(episode_key(part)) if re.match(r"^\d+(?:\.\d+)?$", part) else None
if item and item not in values:
values.append(item)
return values
@@ -145,18 +159,18 @@ def requested_episode_numbers(spec, episodes):
def wanted_episodes(spec, episodes):
by_number = {str(ep.get("number")).rstrip("0").rstrip("."): ep for ep in episodes or []}
by_number = {episode_key(ep.get("number")): ep for ep in episodes or []}
wanted = []
for number in requested_episode_numbers(spec, episodes):
key = str(number).rstrip("0").rstrip(".")
key = episode_key(number)
wanted.append(by_number.get(key) or {"number": number, "id": ""})
return wanted
def episode_by_number(number, episodes):
wanted = str(number).rstrip("0").rstrip(".")
wanted = episode_key(number)
for episode in episodes or []:
current = str(episode.get("number")).rstrip("0").rstrip(".")
current = episode_key(episode.get("number"))
if current == wanted:
return episode
return None
@@ -170,6 +184,62 @@ def ffmpeg_headers(headers):
return "\r\n".join(pairs) + ("\r\n" if pairs else "")
def best_hls_variant_url(master_url, playlist_text):
variants = []
pending_bandwidth = 0
for raw_line in str(playlist_text or "").splitlines():
line = raw_line.strip()
if not line:
continue
if line.startswith("#EXT-X-STREAM-INF"):
match = re.search(r"BANDWIDTH=(\d+)", line)
pending_bandwidth = int(match.group(1)) if match else 0
continue
if pending_bandwidth and not line.startswith("#"):
variants.append((pending_bandwidth, urljoin(master_url, line)))
pending_bandwidth = 0
if not variants:
return master_url
variants.sort(key=lambda item: item[0], reverse=True)
return variants[0][1]
def resolve_hls_input_url(stream):
url = stream.get("url")
if not stream.get("isM3U8") and ".m3u8" not in str(url):
return url
headers = {"User-Agent": "Mozilla/5.0", **(stream.get("headers") or {})}
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=30) as response:
content_type = response.headers.get("Content-Type", "")
data = response.read(2_000_000).decode("utf-8", errors="replace")
if "#EXT-X-STREAM-INF" not in data and "mpegurl" not in content_type.lower():
return url
return best_hls_variant_url(url, data)
def ffmpeg_base_command(stream, input_url):
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel",
"info",
"-y",
"-protocol_whitelist",
"file,http,https,tcp,tls,crypto",
"-allowed_extensions",
"ALL",
]
headers = ffmpeg_headers(stream.get("headers") or {})
if headers:
cmd.extend(["-headers", headers])
referer = (stream.get("headers") or {}).get("Referer")
if referer:
cmd.extend(["-referer", referer])
cmd.extend(["-i", input_url])
return cmd
def download_subtitles(subtitles, output_base):
saved = []
english = [
@@ -188,15 +258,60 @@ def download_subtitles(subtitles, output_base):
def download_episode(stream, target):
cmd = ["ffmpeg", "-hide_banner", "-loglevel", "info", "-y"]
headers = ffmpeg_headers(stream.get("headers") or {})
if headers:
cmd.extend(["-headers", headers])
referer = (stream.get("headers") or {}).get("Referer")
if referer:
cmd.extend(["-referer", referer])
cmd.extend(["-i", stream["url"], "-c", "copy", "-bsf:a", "aac_adtstoasc", str(target)])
return subprocess.call(cmd)
input_url = resolve_hls_input_url(stream)
partial = target.with_suffix(target.suffix + ".part")
for path in (target, partial):
try:
path.unlink()
except FileNotFoundError:
pass
attempts = [
[
*ffmpeg_base_command(stream, input_url),
"-map",
"0:v:0?",
"-map",
"0:a:0?",
"-c",
"copy",
"-bsf:a",
"aac_adtstoasc",
"-movflags",
"+faststart",
"-f",
"mp4",
str(partial),
],
[
*ffmpeg_base_command(stream, input_url),
"-map",
"0",
"-c",
"copy",
"-bsf:a",
"aac_adtstoasc",
"-movflags",
"+faststart",
"-f",
"mp4",
str(partial),
],
]
code = 1
for cmd in attempts:
try:
partial.unlink()
except FileNotFoundError:
pass
code = subprocess.call(cmd)
if code == 0 and partial.exists() and partial.stat().st_size > 0:
partial.replace(target)
return 0
try:
partial.unlink()
except FileNotFoundError:
pass
return code
def provider_episode_candidates(primary_provider, primary_show_id, title, wanted_episode):