Installation guidée + intégration bibliothèque Plex/Sonarr

- scripts/install.sh : premier setup interactif (destination des épisodes,
  port, secrets auto-générés, montage, démarrage) — options --dir/--port/
  --skip-login pour une install non interactive
- Épisodes rangés par animé sur le disque : <Animé>/<Animé> - E12 (VOSTFR).mp4
  (meilleur parsing Plex, suppression du dossier quand il se vide)
- Entrypoint : chown récursif uniquement si le dossier de téléchargements est
  vide — une bibliothèque Plex/Sonarr existante n'est jamais modifiée
- Scan disque récursif (sous-dossiers par animé restaurés)
- README : installation guidée + section « Bibliothèque Plex / Sonarr »
This commit is contained in:
Roman
2026-09-22 12:38:18 +00:00
parent 9500a84a5e
commit 1f326d34dd
6 changed files with 294 additions and 33 deletions
+46 -21
View File
@@ -63,6 +63,19 @@ def sanitize_filename(name: str) -> str:
return name[:150]
_SERIES_RE = re.compile(r"^(.*?)[\s\-–—]*(?:épisode|episode|ep|e)\s*\d", re.IGNORECASE)
def series_dirname(title: str) -> str | None:
"""Nom du sous-dossier d'un titre (« One Piece - E12 » → « One Piece »).
Range les épisodes par animé sur le disque (bien meilleur parsing Plex).
None si aucun marqueur d'épisode détecté → fichier à plat.
"""
match = _SERIES_RE.match(title)
base = sanitize_filename(match.group(1).strip()) if match else ""
return base or None
class DownloadManager:
"""File d'attente de téléchargements, injectée dans les routes via app.state."""
@@ -117,25 +130,29 @@ class DownloadManager:
download_dir = get_settings().download_dir
rows = await db.fetchall("SELECT file_path FROM downloads WHERE file_path IS NOT NULL")
known = {row["file_path"] for row in rows}
for path in download_dir.iterdir():
if path.is_file() and path.suffix != ".part" and path.name not in known:
size = path.stat().st_size
await db.execute(
"INSERT INTO downloads "
"(source_key, video_url, page_url, title, file_path, status, "
" total_bytes, downloaded_bytes) "
"VALUES (?, ?, ?, ?, ?, 'done', ?, ?)",
(
f"file:{path.name}",
"",
"",
path.stem,
path.name,
size,
size,
),
)
logger.info("Fichier restauré depuis le disque : %s", path.name)
for path in download_dir.rglob("*"):
if not path.is_file() or path.suffix == ".part":
continue
rel_path = path.relative_to(download_dir).as_posix()
if rel_path in known:
continue
size = path.stat().st_size
await db.execute(
"INSERT INTO downloads "
"(source_key, video_url, page_url, title, file_path, status, "
" total_bytes, downloaded_bytes) "
"VALUES (?, ?, ?, ?, ?, 'done', ?, ?)",
(
f"file:{rel_path}",
"",
"",
path.stem,
rel_path,
size,
size,
),
)
logger.info("Fichier restauré depuis le disque : %s", rel_path)
# ------------------------------------------------------------ API publique
@@ -152,10 +169,12 @@ class DownloadManager:
return self._to_dict(existing, duplicate=True)
filename = sanitize_filename(title) + self._guess_extension(video_url)
series = series_dirname(title)
file_path = f"{series}/{filename}" if series else filename
cursor = await db.execute(
"INSERT INTO downloads (source_key, video_url, page_url, title, file_path) "
"VALUES (?, ?, ?, ?, ?)",
(source_key, video_url, page_url, title, filename),
(source_key, video_url, page_url, title, file_path),
)
download_id = cursor.lastrowid
await self._queue.put(download_id)
@@ -234,7 +253,11 @@ class DownloadManager:
if row["status"] in ACTIVE_STATUSES:
await self.cancel(download_id)
if delete_file and row["file_path"]:
(get_settings().download_dir / row["file_path"]).unlink(missing_ok=True)
target = get_settings().download_dir / row["file_path"]
target.unlink(missing_ok=True)
if target.parent != get_settings().download_dir:
with contextlib.suppress(OSError): # dossier de l'animé vide → retiré
target.parent.rmdir()
self._part_path(row["file_path"]).unlink(missing_ok=True)
await db.execute("DELETE FROM downloads WHERE id = ?", (download_id,))
await self._emit_removed(download_id)
@@ -324,6 +347,7 @@ class DownloadManager:
"""Téléchargement HTTP direct avec reprise via Range."""
video_url, file_path = row["video_url"], row["file_path"]
target = get_settings().download_dir / file_path
target.parent.mkdir(parents=True, exist_ok=True)
part = self._part_path(file_path)
downloaded = part.stat().st_size if part.exists() else 0
@@ -377,6 +401,7 @@ class DownloadManager:
"""
video_url, file_path = row["video_url"], row["file_path"]
target = get_settings().download_dir / file_path
target.parent.mkdir(parents=True, exist_ok=True)
part = self._part_path(file_path)
part.unlink(missing_ok=True) # pas de reprise partielle en HLS