From d4dca46d9f494c43e3cf10f9a8765c4f37467636 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 13 Aug 2026 00:47:51 +0200 Subject: [PATCH] fix(downloads): keep sidecar basenames intact when the title contains a dot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit write_sidecars / delete_asset_files computed `base = media.with_suffix("")` and then `base.with_suffix(".nfo")`. Stripping the real `.mp4` first leaves a stem whose own last dot may be inside the title (e.g. "Sonka Házilag."), which `with_suffix` then treats as the extension and clobbers — writing the .nfo/.jpg under a TRUNCATED basename that no longer matches the media, so Plex/Kodi never associate them (and delete_asset_files targets the wrong name, leaking orphans). Swap the extension on the FULL media path via a shared `_sidecar` helper instead. Pre-existing bug (predates the source-adapters work); found during its E2E and folded in here. Verified end-to-end: a dotted-title Facebook reel now writes a single shared basename. --- backend/app/downloads/storage.py | 16 +++++++++++----- backend/tests/test_storage.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/backend/app/downloads/storage.py b/backend/app/downloads/storage.py index cb405f3b..ec21cf3e 100644 --- a/backend/app/downloads/storage.py +++ b/backend/app/downloads/storage.py @@ -181,16 +181,23 @@ def _nfo_xml(meta: MediaMeta) -> str: return "\n".join(parts) +def _sidecar(media: Path, ext: str) -> Path: + """The sibling sidecar path (`.nfo` / `.jpg`) for a media file. Swaps the extension on the FULL + media path — NOT via `media.with_suffix("")` first: stripping the real extension leaves a stem + whose OWN last dot may be inside the title (e.g. "Sonka Házilag."), which `with_suffix` would + then read as the extension and clobber, orphaning the sidecar under a truncated name.""" + return media.with_suffix(ext) + + def write_sidecars( download_root: str, rel: str, meta: MediaMeta, thumb_src: Path | None ) -> bool: """Write `.nfo` + `.jpg` next to the media file. Returns True if written.""" media = abs_path(download_root, rel) - base = media.with_suffix("") try: - base.with_suffix(".nfo").write_text(_nfo_xml(meta), encoding="utf-8") + _sidecar(media, ".nfo").write_text(_nfo_xml(meta), encoding="utf-8") if thumb_src and thumb_src.exists(): - shutil.copyfile(str(thumb_src), str(base.with_suffix(".jpg"))) + shutil.copyfile(str(thumb_src), str(_sidecar(media, ".jpg"))) return True except OSError: return False @@ -199,8 +206,7 @@ def write_sidecars( def delete_asset_files(download_root: str, rel: str) -> None: """Remove the media file and its sidecars; prune now-empty parent dirs (best effort).""" media = abs_path(download_root, rel) - base = media.with_suffix("") - for p in (media, base.with_suffix(".nfo"), base.with_suffix(".jpg")): + for p in (media, _sidecar(media, ".nfo"), _sidecar(media, ".jpg")): try: p.unlink() except OSError: diff --git a/backend/tests/test_storage.py b/backend/tests/test_storage.py index 29b7fccb..d467e9a4 100644 --- a/backend/tests/test_storage.py +++ b/backend/tests/test_storage.py @@ -147,3 +147,26 @@ def test_nfo_uniqueid_type_reflects_the_source(): base = dict(video_id="v1", title="t", uploader="c", upload_date=None) assert '