diff --git a/app/services/downloads.py b/app/services/downloads.py index c0c8508..16f21e3 100644 --- a/app/services/downloads.py +++ b/app/services/downloads.py @@ -76,6 +76,33 @@ def series_dirname(title: str) -> str | None: base = sanitize_filename(match.group(1).strip()) if match else "" return base or None + +_EPISODE_RE = re.compile( + r"^(?P.+?)\s*-\s*(?:Saison\s+(?P\d+)\s*-\s*)?" + r"E(?P\d+(?:[.,]\d+)?)(?P\s*\([^)]*\))?\s*$", + re.IGNORECASE, +) + + +def plex_filename(title: str, extension: str) -> str | None: + """Nom de fichier compatible Plex (« S01E09 ») pour un titre d'épisode. + + Les scrapers produisent « Série - Saison 1 - E9 (VF) » ; le scanner Plex + exige « Série - S01E09 (VF) » (les épisodes 1-9 sont sinon ignorés). + None si le titre ne suit pas le format épisode → nommage inchangé. + """ + match = _EPISODE_RE.match(title) + if not match: + return None + series = match.group("series") + season = int(match.group("season") or 1) + ep_raw = match.group("ep").replace(",", ".") + ep = float(ep_raw) + ep_label = f"{int(ep):02d}" if ep.is_integer() else ep_raw + tail = match.group("tail") or "" + return sanitize_filename(f"{series} - S{season:02d}E{ep_label}{tail}") + extension + + class DownloadManager: """File d'attente de téléchargements, injectée dans les routes via app.state.""" @@ -174,7 +201,8 @@ class DownloadManager: logger.info("Anti-doublon : %s déjà en file (id=%s)", title, existing["id"]) return self._to_dict(existing, duplicate=True) - filename = sanitize_filename(title) + self._guess_extension(video_url) + extension = self._guess_extension(video_url) + filename = plex_filename(title, extension) or sanitize_filename(title) + extension series = series_dirname(title) file_path = f"{series}/{filename}" if series else filename cursor = await db.execute( diff --git a/tests/test_core.py b/tests/test_core.py index 6908726..0fa804d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -10,7 +10,7 @@ from app.scrapers.base import ( import_all_scrapers, resolve_hoster, ) -from app.services.downloads import sanitize_filename +from app.services.downloads import plex_filename, sanitize_filename # ------------------------------------------------------------ format interne @@ -52,6 +52,32 @@ def test_sanitize_no_traversal(): assert sanitize_filename("../../etc/passwd") == "etc passwd" +# ------------------------------------------------------------ nommage Plex + + +@pytest.mark.parametrize( + ("title", "expected"), + [ + ( + "The Eminence in Shadow - Saison 1 - E9 (VF)", + "The Eminence in Shadow - S01E09 (VF).mp4", + ), + ( + "The Eminence in Shadow - Saison 1 - E10 (VF)", + "The Eminence in Shadow - S01E10 (VF).mp4", + ), + ("Anime X - E1 (VOSTFR)", "Anime X - S01E01 (VOSTFR).mp4"), + ("One Piece - E1122", "One Piece - S01E1122.mp4"), + ("Série - saison 2 - e3 (vf)", "Série - S02E03 (vf).mp4"), + ("Episode Test 1", None), + ("Mon Titre", None), + ("Series S01E05 VOSTFR WEB-DL", None), + ], +) +def test_plex_filename(title, expected): + assert plex_filename(title, ".mp4") == expected + + # ------------------------------------------------------------ auth diff --git a/tests/test_downloads.py b/tests/test_downloads.py index 30fd302..3816ef7 100644 --- a/tests/test_downloads.py +++ b/tests/test_downloads.py @@ -139,7 +139,7 @@ async def test_series_subfolder_and_scan(file_server, manager): d = await dm.enqueue(f"{file_server}/video.mp4", "http://page", "Anime Z - E7 (VOSTFR)") data = await _wait_status(d["id"], {"done", "failed"}) assert data["status"] == "done", data.get("error") - assert data["file_path"] == "Anime Z/Anime Z - E7 (VOSTFR).mp4" + assert data["file_path"] == "Anime Z/Anime Z - S01E07 (VOSTFR).mp4" assert (get_settings().download_dir / data["file_path"]).read_bytes() == PAYLOAD # DB vidée + redémarrage → le fichier en sous-dossier est restauré tel quel @@ -148,4 +148,4 @@ async def test_series_subfolder_and_scan(file_server, manager): await dm.start() items = await dm.list_all() paths = [i["file_path"] for i in items] # d'autres fichiers de tests peuvent trainer - assert "Anime Z/Anime Z - E7 (VOSTFR).mp4" in paths + assert "Anime Z/Anime Z - S01E07 (VOSTFR).mp4" in paths