Query Arte Player API before each download to determine available stream versions. Select lang tag (VOSTFR > VO, FRENCH if audio is fr). Embed French subtitles as default MKV track when VOSTFR. All output now .mkv. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+29
@@ -278,6 +278,35 @@ async def fetch_concerts(page: int = 1, search: str = "", page_size: int = 24, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_versions(pid: str) -> list[dict]:
|
||||||
|
"""Fetch available stream versions from Arte Player API for a programme ID."""
|
||||||
|
try:
|
||||||
|
raw = _fetch_url(
|
||||||
|
PLAYER_API.format(pid=pid),
|
||||||
|
headers={"User-Agent": _HEADERS["User-Agent"], "Accept": "application/json"},
|
||||||
|
)
|
||||||
|
data = json.loads(raw)
|
||||||
|
streams = data["data"]["attributes"].get("streams") or []
|
||||||
|
return streams[0].get("versions") or [] if streams else []
|
||||||
|
except Exception as ex:
|
||||||
|
logger.debug("Failed to get versions for %s: %s", pid, ex)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def select_lang_tag(versions: list[dict]) -> str:
|
||||||
|
"""
|
||||||
|
Determine UNFR language tag from stream versions.
|
||||||
|
FR audio → FRENCH, non-FR + FR subs → VOSTFR, otherwise → VO.
|
||||||
|
"""
|
||||||
|
if not versions:
|
||||||
|
return "VO"
|
||||||
|
if any(v.get("audioLanguage") == "fr" for v in versions):
|
||||||
|
return "FRENCH"
|
||||||
|
if any(v.get("subtitleLanguage") == "fr" for v in versions):
|
||||||
|
return "VOSTFR"
|
||||||
|
return "VO"
|
||||||
|
|
||||||
|
|
||||||
async def invalidate_cache() -> int:
|
async def invalidate_cache() -> int:
|
||||||
_cache["ts"] = 0
|
_cache["ts"] = 0
|
||||||
try:
|
try:
|
||||||
|
|||||||
+29
-12
@@ -9,7 +9,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
|
from arte_api import get_versions, select_lang_tag
|
||||||
|
|
||||||
OUTPUT_DIR = "/data/Arte"
|
OUTPUT_DIR = "/data/Arte"
|
||||||
|
_PID_RE = re.compile(r"\b(\d{6}-\d{3}-[A-Z])\b")
|
||||||
DB_PATH = "data/arte_dl.db"
|
DB_PATH = "data/arte_dl.db"
|
||||||
|
|
||||||
Path("data").mkdir(exist_ok=True)
|
Path("data").mkdir(exist_ok=True)
|
||||||
@@ -39,12 +42,10 @@ def _slugify(s: str) -> str:
|
|||||||
return s.strip(".")
|
return s.strip(".")
|
||||||
|
|
||||||
|
|
||||||
def build_release_name(title: str, subtitle: str, year: int | None, info: dict) -> str:
|
def build_release_name(title: str, subtitle: str, year: int | None, info: dict, lang_tag: str = "VO") -> str:
|
||||||
|
"""Build a proper UNFR/scene release name.
|
||||||
|
Format: Title.Event.Year.LANG.Resolution.WEB-DL.x264.AAC-ReMoRa.mkv
|
||||||
"""
|
"""
|
||||||
Build a proper UNFR/scene release name.
|
|
||||||
Format: Title.Event.Year.FRENCH.Resolution.WEBRip.x264.AAC-ReMoRa.mp4
|
|
||||||
"""
|
|
||||||
# Strip year from both title and subtitle to avoid duplication
|
|
||||||
t = re.sub(r"\b" + str(year) + r"\b", "", title).strip() if year else title
|
t = re.sub(r"\b" + str(year) + r"\b", "", title).strip() if year else title
|
||||||
name = _slugify(t)
|
name = _slugify(t)
|
||||||
|
|
||||||
@@ -57,7 +58,6 @@ def build_release_name(title: str, subtitle: str, year: int | None, info: dict)
|
|||||||
|
|
||||||
year_str = str(year) if year else ""
|
year_str = str(year) if year else ""
|
||||||
|
|
||||||
# Resolution from yt-dlp info
|
|
||||||
height = info.get("height") or 0
|
height = info.get("height") or 0
|
||||||
if height >= 2160:
|
if height >= 2160:
|
||||||
res = "2160p"
|
res = "2160p"
|
||||||
@@ -68,7 +68,6 @@ def build_release_name(title: str, subtitle: str, year: int | None, info: dict)
|
|||||||
else:
|
else:
|
||||||
res = f"{height}p" if height else "1080p"
|
res = f"{height}p" if height else "1080p"
|
||||||
|
|
||||||
# Video codec (avc1 = H.264, hev1/hvc1/hevc = H.265)
|
|
||||||
vcodec = (info.get("vcodec") or "").lower()
|
vcodec = (info.get("vcodec") or "").lower()
|
||||||
if "hevc" in vcodec or "h265" in vcodec or "hev1" in vcodec or "hvc1" in vcodec:
|
if "hevc" in vcodec or "h265" in vcodec or "hev1" in vcodec or "hvc1" in vcodec:
|
||||||
vc = "HEVC"
|
vc = "HEVC"
|
||||||
@@ -77,9 +76,9 @@ def build_release_name(title: str, subtitle: str, year: int | None, info: dict)
|
|||||||
else:
|
else:
|
||||||
vc = "x264"
|
vc = "x264"
|
||||||
|
|
||||||
parts = [name, year_str, res, "WEB-DL", vc, "AAC"]
|
parts = [name, year_str, lang_tag, res, "WEB-DL", vc, "AAC"]
|
||||||
base = ".".join(p for p in parts if p)
|
base = ".".join(p for p in parts if p)
|
||||||
return f"{base}-ReMoRa.mp4"
|
return f"{base}-ReMoRa.mkv"
|
||||||
|
|
||||||
|
|
||||||
class DownloadManager:
|
class DownloadManager:
|
||||||
@@ -192,6 +191,13 @@ class DownloadManager:
|
|||||||
with _db() as conn:
|
with _db() as conn:
|
||||||
conn.execute("UPDATE downloads SET state='downloading' WHERE id=?", (dl_id,))
|
conn.execute("UPDATE downloads SET state='downloading' WHERE id=?", (dl_id,))
|
||||||
|
|
||||||
|
# Determine language tag from Arte Player API before downloading
|
||||||
|
pid_m = _PID_RE.search(url)
|
||||||
|
lang_tag = "VO"
|
||||||
|
if pid_m:
|
||||||
|
versions = get_versions(pid_m.group(1))
|
||||||
|
lang_tag = select_lang_tag(versions)
|
||||||
|
|
||||||
# For HLS, yt-dlp downloads video then audio separately.
|
# For HLS, yt-dlp downloads video then audio separately.
|
||||||
# After the first stream finishes, stay in "processing" to avoid
|
# After the first stream finishes, stay in "processing" to avoid
|
||||||
# resetting progress to 0% when the audio stream starts.
|
# resetting progress to 0% when the audio stream starts.
|
||||||
@@ -216,19 +222,30 @@ class DownloadManager:
|
|||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
"outtmpl": f"{out_dir}/%(title)s.%(ext)s",
|
"outtmpl": f"{out_dir}/%(title)s.%(ext)s",
|
||||||
"format": "bestvideo[vcodec^=avc1]+bestaudio/bestvideo+bestaudio/best",
|
"format": "bestvideo[vcodec^=avc1]+bestaudio/bestvideo+bestaudio/best",
|
||||||
"merge_output_format": "mp4",
|
"merge_output_format": "mkv",
|
||||||
"progress_hooks": [hook],
|
"progress_hooks": [hook],
|
||||||
"quiet": True,
|
"quiet": True,
|
||||||
"no_warnings": True,
|
"no_warnings": True,
|
||||||
}
|
}
|
||||||
|
if lang_tag == "VOSTFR":
|
||||||
|
ydl_opts.update({
|
||||||
|
"writesubtitles": True,
|
||||||
|
"subtitleslangs": ["fr"],
|
||||||
|
"embedsubtitles": True,
|
||||||
|
# Set first subtitle track as default in MKV
|
||||||
|
"postprocessor_args": {"ffmpeg_o": ["-disposition:s:0", "default"]},
|
||||||
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(url, download=True)
|
info = ydl.extract_info(url, download=True)
|
||||||
orig_path = Path(ydl.prepare_filename(info))
|
orig_path = Path(ydl.prepare_filename(info))
|
||||||
|
|
||||||
# Rename to proper release name
|
# yt-dlp renames to .mkv after merge; prepare_filename may return .mp4
|
||||||
release_name = build_release_name(title, subtitle, year, info)
|
if not orig_path.exists():
|
||||||
|
orig_path = orig_path.with_suffix(".mkv")
|
||||||
|
|
||||||
|
release_name = build_release_name(title, subtitle, year, info, lang_tag)
|
||||||
dest_path = orig_path.parent / release_name
|
dest_path = orig_path.parent / release_name
|
||||||
if orig_path.exists() and orig_path != dest_path:
|
if orig_path.exists() and orig_path != dest_path:
|
||||||
if dest_path.exists():
|
if dest_path.exists():
|
||||||
|
|||||||
Reference in New Issue
Block a user