fix(downloads): a YouTube clip outside the catalog takes its title from the fetch

It was queued as "Clip (clip)" and stayed so: nothing renamed it once yt-dlp had answered.
This commit is contained in:
2026-10-10 02:43:13 +02:00
parent 14598ec320
commit d466ae915b
3 changed files with 44 additions and 13 deletions
+21 -9
View File
@@ -907,9 +907,12 @@ def _process_edit(job_id: int, asset_id: int) -> None:
shutil.rmtree(staging, ignore_errors=True)
def _clip_source(job_id: int, asset_id: int, spec: dict, staging: Path) -> tuple[Path, float]:
"""The file a clip is cut from and where in it the clip starts. A YouTube clip fetches ONLY its
range into `staging` (so it starts at 0 there); a Plex clip reads the library file in place."""
def _clip_source(
job_id: int, asset_id: int, spec: dict, staging: Path
) -> tuple[Path, float, str | None]:
"""The file a clip is cut from, where in it the clip starts, and the source's title when only
the fetch could tell it. A YouTube clip fetches ONLY its range into `staging` (so it starts at
0 there); a Plex clip reads the library file in place."""
if spec["source"] == "youtube":
with SessionLocal() as db:
job = db.get(DownloadJob, job_id)
@@ -929,7 +932,9 @@ def _clip_source(job_id: int, asset_id: int, spec: dict, staging: Path) -> tuple
max_bytes=remaining,
section=(spec["start_s"], spec["end_s"]),
)
return _produced_file(_media_info(dict(info)), staging), 0.0
info = _media_info(dict(info))
title = normalize_title(adapters.resolve_adapter("youtube", spec["ref"]).title(info))
return _produced_file(info, staging), 0.0, title
with SessionLocal() as db:
item = db.execute(
select(PlexItem).where(PlexItem.rating_key == spec["ref"])
@@ -937,7 +942,7 @@ def _clip_source(job_id: int, asset_id: int, spec: dict, staging: Path) -> tuple
src = plex_paths.local_media_path(db, item.file_path) if item else None
if src is None:
raise StatedFailure("The library file is not reachable from here.")
return src, spec["start_s"]
return src, spec["start_s"], None
def _process_clip(job_id: int, asset_id: int) -> None:
@@ -954,7 +959,7 @@ def _process_clip(job_id: int, asset_id: int) -> None:
if staging.exists():
shutil.rmtree(staging, ignore_errors=True)
staging.mkdir(parents=True, exist_ok=True)
src, start = _clip_source(job_id, asset_id, spec, staging)
src, start, fetched_title = _clip_source(job_id, asset_id, spec, staging)
data = probe.probe_json(src) or {}
audio_track = spec.get("audio_track")
if audio_track is not None and audio_track >= clipmod.audio_track_count(data):
@@ -1008,10 +1013,17 @@ def _process_clip(job_id: int, asset_id: int) -> None:
asset.error = None
asset.last_access_at = datetime.now(timezone.utc)
asset.expires_at = datetime.now(timezone.utc) + timedelta(days=retention_days)
fields: dict = {"status": JobStatus.DONE, "progress": 100, "phase": None}
# A video outside the catalog was queued as "Clip (clip)": the fetch has its title now.
placeholder = service.clip_title(None, None)
if asset and fetched_title and asset.title == placeholder:
named = service.clip_title(fetched_title, None)
asset.title = named
if job_row := db.get(DownloadJob, job_id):
if job_row.display_name == placeholder:
fields["display_name"] = named[:DISPLAY_TEXT_MAX]
title = asset.title if asset else None
finalized = service.finalize_if_running(
db, job_id, status=JobStatus.DONE, progress=100, phase=None
)
finalized = service.finalize_if_running(db, job_id, **fields)
db.commit()
if finalized:
_notify_download_done(db, user_id, title, job_id, kind="edit")
+4 -4
View File
@@ -210,7 +210,7 @@ def enqueue(
return job
def _clip_title(source_title: str | None, display_name: str | None) -> str:
def clip_title(source_title: str | None, display_name: str | None) -> str:
# Clipped to DISPLAY_TEXT_MAX, not to a literal: this feeds `DownloadJob.display_name` through
# `enqueue_edit`, the same column `display_text` guards. A machine-made title, so it is clipped
# rather than run through `display_text` — blank here means "no title", not None.
@@ -252,7 +252,7 @@ def enqueue_edit(
ref = f"{user_id}:{src_asset.id}"[:512]
asset = get_or_create_asset(db, "edit", ref, sig)
if not asset.title:
asset.title = _clip_title(src_asset.title, display_name)
asset.title = clip_title(src_asset.title, display_name)
asset.uploader = src_asset.uploader
asset.thumbnail_url = src_asset.thumbnail_url
asset.upload_date = src_asset.upload_date
@@ -294,7 +294,7 @@ def _clip_source_facts(db: Session, spec: dict) -> dict:
rather than as a job that fails in the queue a minute later."""
if spec["source"] == "youtube":
video = db.get(Video, spec["ref"])
if video is None: # not in the catalog: the worker fills the title from yt-dlp's answer
if video is None: # not in the catalog: titled "Clip" until yt-dlp names it (worker)
return {"title": None, "uploader": None, "thumbnail_url": None, "length_s": None}
channel = db.get(Channel, video.channel_id)
return {
@@ -346,7 +346,7 @@ def enqueue_clip(
ref = clipmod.asset_ref(user_id, spec)[:512]
asset = get_or_create_asset(db, "clip", ref, clipmod.sig(spec))
if not asset.title:
asset.title = _clip_title(facts["title"], display_name)
asset.title = clip_title(facts["title"], display_name)
asset.uploader = facts["uploader"]
asset.thumbnail_url = facts["thumbnail_url"]
asset.duration_s = round(clipmod.duration(spec))
+19
View File
@@ -421,3 +421,22 @@ def test_service_reference_url_follows_the_clip_spec(db, make_user, as_user, yt_
job = _clip(as_user(make_user()), source="youtube", ref=yt_video, start_s=65, end_s=90).json()
row = db.get(DownloadJob, job["id"])
assert service.reference_url(row, None) == f"https://www.youtube.com/watch?v={yt_video}&t=65s"
def test_a_youtube_clip_outside_the_catalog_takes_its_title_from_the_fetch(
db, make_user, as_user, worker, monkeypatch
):
def extract(job_id, asset_id, url, spec, staging, adapter, **kw):
f = Path(staging) / "zyxwvutsrqp.mp4"
f.write_bytes(b"range")
return {"requested_downloads": [{"filepath": str(f)}], "title": "Found On YouTube"}
monkeypatch.setattr(pipeline, "_extract_with_fallback", extract)
job = _clip(as_user(make_user()), source="youtube", ref="zyxwvutsrqp").json()
assert job["display_name"] == "Clip (clip)"
job, asset = worker["run"](job)
assert (job.status, asset.title, job.display_name) == (
"done",
"Found On YouTube (clip)",
"Found On YouTube (clip)",
)