From 124afb6d20cc72d2099c79278067fadde768139b Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 26 Apr 2026 13:48:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20nommage=20UNFR=20-ReMoRa=20automatique?= =?UTF-8?q?=20apr=C3=A8s=20t=C3=A9l=C3=A9chargement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Format : Title.Event.Year.FRENCH.Resolution.WEBRip.x264|HEVC.AAC-ReMoRa.mp4 - build_release_name() : slugify avec strip accents, apostrophe→point, déduplique l'année si présente dans le titre ET passée séparément, détecte la résolution et le codec depuis les infos yt-dlp - enqueue() : reçoit subtitle + year depuis l'API - _run() : renomme le fichier après download, met à jour le filename en DB - DownloadRequest : subtitle + year ajoutés - app.js : extrait l'année du subtitle via regex avant d'envoyer la requête Co-Authored-By: Claude Sonnet 4.6 --- downloader.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++---- main.py | 4 ++- static/app.js | 4 ++- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/downloader.py b/downloader.py index 7bf45e0..4a50e29 100644 --- a/downloader.py +++ b/downloader.py @@ -1,5 +1,7 @@ +import re import sqlite3 import threading +import unicodedata import uuid from datetime import datetime from pathlib import Path @@ -17,6 +19,67 @@ def _db(): return conn +# ── Release naming ───────────────────────────────────────────────────────────── + +def _slugify(s: str) -> str: + """Normalize a string to dot-separated scene-style slug.""" + # Strip accents (NFD decompose then drop combining marks) + s = unicodedata.normalize("NFD", s) + s = "".join(c for c in s if unicodedata.category(c) != "Mn") + # Apostrophe before letter → .Letter (L'Amour → .L.Amour) + s = re.sub(r"['’]([A-Za-z])", lambda m: "." + m.group(1).upper(), s) + # Spaces / underscores → dot + s = re.sub(r"[\s_]+", ".", s) + # Keep only alphanumeric, dot, hyphen + s = re.sub(r"[^A-Za-z0-9.\-]", "", s) + # Collapse multiple dots + s = re.sub(r"\.{2,}", ".", s) + return s.strip(".") + + +def build_release_name(title: str, subtitle: str, year: int | None, info: dict) -> str: + """ + 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 + name = _slugify(t) + + sub = subtitle or "" + if year: + sub = re.sub(r"\b" + str(year) + r"\b", "", sub).strip() + sub_slug = _slugify(sub) + if sub_slug: + name = f"{name}.{sub_slug}" + + year_str = str(year) if year else "" + + # Resolution from yt-dlp info + height = info.get("height") or 0 + if height >= 2160: + res = "2160p" + elif height >= 1080: + res = "1080p" + elif height >= 720: + res = "720p" + else: + res = f"{height}p" if height else "1080p" + + # Video codec (avc1 = H.264, hev1/hvc1/hevc = H.265) + vcodec = (info.get("vcodec") or "").lower() + if "hevc" in vcodec or "h265" in vcodec or "hev1" in vcodec or "hvc1" in vcodec: + vc = "HEVC" + elif "avc" in vcodec or "h264" in vcodec: + vc = "x264" + else: + vc = "x264" + + parts = [name, year_str, "FRENCH", res, "WEBRip", vc, "AAC"] + base = ".".join(p for p in parts if p) + return f"{base}-ReMoRa.mp4" + + class DownloadManager: def __init__(self): self._active: dict[str, dict] = {} @@ -43,7 +106,8 @@ class DownloadManager: # ------------------------------------------------------------------ public - def enqueue(self, url: str, title: str, bg: BackgroundTasks) -> str: + def enqueue(self, url: str, title: str, subtitle: str, year: int | None, + bg: BackgroundTasks) -> str: dl_id = str(uuid.uuid4()) now = datetime.now().isoformat() with _db() as conn: @@ -53,7 +117,7 @@ class DownloadManager: ) with self._lock: self._active[dl_id] = {"state": "queued", "progress": 0, "title": title} - bg.add_task(self._run, dl_id, url) + bg.add_task(self._run, dl_id, url, title, subtitle, year) return dl_id def status(self, dl_id: str) -> dict: @@ -80,15 +144,15 @@ class DownloadManager: with self._lock: self._active.setdefault(dl_id, {}).update(kw) - def _run(self, dl_id: str, url: str): + def _run(self, dl_id: str, url: str, title: str, subtitle: str, year: int | None): Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True) self._set(dl_id, state="downloading") with _db() as conn: conn.execute("UPDATE downloads SET state='downloading' WHERE id=?", (dl_id,)) # For HLS, yt-dlp downloads video then audio separately. - # After the first stream finishes, stay in "processing" — don't reset - # to "downloading" when the audio stream starts. + # After the first stream finishes, stay in "processing" to avoid + # resetting progress to 0% when the audio stream starts. finished_once = [False] def hook(d): @@ -119,7 +183,17 @@ class DownloadManager: try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True) - filename = ydl.prepare_filename(info) + orig_path = Path(ydl.prepare_filename(info)) + + # Rename to proper release name + release_name = build_release_name(title, subtitle, year, info) + dest_path = orig_path.parent / release_name + if orig_path.exists() and orig_path != dest_path: + if dest_path.exists(): + dest_path.unlink() + orig_path.rename(dest_path) + filename = str(dest_path) + self._set(dl_id, state="done", progress=100) with _db() as conn: conn.execute( diff --git a/main.py b/main.py index aa5a253..f2a8c34 100644 --- a/main.py +++ b/main.py @@ -53,13 +53,15 @@ async def api_refresh(): class DownloadRequest(BaseModel): url: str title: str + subtitle: str = "" + year: int | None = None @app.post("/api/download") async def api_download(req: DownloadRequest, bg: BackgroundTasks): if not req.url: raise HTTPException(status_code=400, detail="url required") - dl_id = dm.enqueue(req.url, req.title, bg) + dl_id = dm.enqueue(req.url, req.title, req.subtitle, req.year, bg) return {"id": dl_id} diff --git a/static/app.js b/static/app.js index 8235e30..6097209 100644 --- a/static/app.js +++ b/static/app.js @@ -265,10 +265,12 @@ $('btn-download').addEventListener('click', async () => { btnDl.textContent = 'Démarrage…'; try { + const yearMatch = (c.subtitle || '').match(/\b(20\d{2})\b/); + const year = yearMatch ? parseInt(yearMatch[1]) : null; const res = await fetch('/api/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: c.url, title: c.title }), + body: JSON.stringify({ url: c.url, title: c.title, subtitle: c.subtitle || '', year }), }); const { id } = await res.json(); trackDownload(id, c.title, c.url);