fix(downloads): disambiguate the media path by asset id (C-3.1)

A MediaAsset is unique on (source_kind, source_ref, format_sig), so the same video in
two formats is two assets — but rel_path keyed only on video_id, so they computed the
same path: the second download silently overwrote the first, and deleting either
removed the shared file. rel_path now takes an optional asset_id and appends "_a{id}"
(which Plex ignores — the .nfo sidecar carries the metadata); the worker passes the
asset id. Only affects NEW downloads (existing assets keep their stored rel_path).
Covered by a new test (two ids → two paths; omitted → the plain name, unchanged).
This commit is contained in:
2026-07-21 21:47:45 +02:00
parent 0dfc5e9ea2
commit 0317f53b1e
2 changed files with 21 additions and 4 deletions
+11 -4
View File
@@ -74,16 +74,23 @@ def display_filename(name: str) -> str:
return text or "download"
def rel_path(meta: MediaMeta, ext: str, layout: str = "plex") -> str:
"""Path of the media file relative to DOWNLOAD_ROOT (forward slashes)."""
def rel_path(meta: MediaMeta, ext: str, layout: str = "plex", asset_id: int | None = None) -> str:
"""Path of the media file relative to DOWNLOAD_ROOT (forward slashes).
`asset_id` disambiguates the file. A MediaAsset is unique on (source_kind, source_ref,
format_sig), so the SAME video downloaded in two formats is two distinct assets — but without
the id they compute the identical path, so the second silently overwrites the first and deleting
either removes the shared file. The `_a{id}` suffix (which Plex ignores — the .nfo sidecar carries
the metadata) keeps them apart. Optional so callers/tests that don't need it keep the plain name."""
title = sanitize(meta.title)
vid = meta.video_id
disc = f"_a{asset_id}" if asset_id is not None else ""
if layout == "flat":
return f"{title}_[{vid}].{ext}"
return f"{title}_[{vid}]{disc}.{ext}"
channel = sanitize(meta.uploader or "Unknown_Channel", 80)
year = meta.upload_date.year if meta.upload_date else 0
d = meta.upload_date.isoformat() if meta.upload_date else "0000-00-00"
epname = f"{channel}_-_{d}_-_{title}_[{vid}].{ext}"
epname = f"{channel}_-_{d}_-_{title}_[{vid}]{disc}.{ext}"
return f"{channel}/Season_{year}/{epname}"
+10
View File
@@ -54,6 +54,16 @@ class TestRelPath:
out = rel_path(meta, "mp4")
assert "Season_0/" in out and "0000-00-00" in out
def test_asset_id_disambiguates_two_formats_of_the_same_video(self):
# Same video, two assets (e.g. 1080p vs 720p) must NOT collide on one path.
a = rel_path(self.meta, "mp4", asset_id=5)
b = rel_path(self.meta, "mp4", asset_id=6)
assert a != b
assert a.endswith("_[abc123]_a5.mp4") and b.endswith("_[abc123]_a6.mp4")
# Omitting it keeps the plain name (unchanged for callers/tests that don't pass one).
assert rel_path(self.meta, "mp4", layout="flat") == "Cool_Clip_[abc123].mp4"
assert rel_path(self.meta, "mp4", layout="flat", asset_id=7) == "Cool_Clip_[abc123]_a7.mp4"
class TestDownloadFilename:
def test_appends_the_container_extension(self):