Fix provider HLS downloads
This commit is contained in:
+130
-15
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user