Skip to content

Commit 9d030db

Browse files
aarroclaude
andcommitted
fix: resolve legacy-agent GUIDs in Fix Thumbnails
_video_id_from_plex_item now parses com.plexapp.agents.youtube-as-movies GUIDs by URL-decoding the embedded file path and running extract_video_id on the filename and parent directory name, with a stem_index fallback for no-bracket filenames. _fix_all_thumbnails accepts and passes stem_index to the resolver so the thread captures a stable reference. Covers all four previously-skipped real-world cases: - MeTube layout with ID in folder name (numeric 10/8-digit IDs) - Short alphanumeric IDs in folder name - No-bracket filenames resolved via stem_index Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 43083f8 commit 9d030db

2 files changed

Lines changed: 100 additions & 5 deletions

File tree

provider/app.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from contextlib import asynccontextmanager
1313
from pathlib import Path
1414
from typing import Literal
15-
from urllib.parse import quote
15+
from urllib.parse import quote, unquote
1616

1717
import httpx
1818
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request
@@ -670,12 +670,38 @@ async def _fetch_plex_sections(client: httpx.AsyncClient) -> list[dict]:
670670
return data.get("MediaContainer", {}).get("Directory", [])
671671

672672

673-
def _video_id_from_plex_item(item) -> str | None:
674-
"""Extract the YAMP video ID from a plexapi library item, or None if not a YAMP item."""
673+
_LEGACY_AGENT_GUID_PREFIX = "com.plexapp.agents.youtube-as-movies://youtube-as-movies|"
674+
675+
676+
def _video_id_from_plex_item(item, stem_index: dict[str, str] | None = None) -> str | None:
677+
"""Extract the video ID from a plexapi library item.
678+
679+
Handles both YAMP GUIDs (tv.plex.agents.custom.yamp://movie/{id}) and legacy
680+
youtube-as-movies agent GUIDs (com.plexapp.agents.youtube-as-movies://youtube-as-movies|{path}|{hash}).
681+
"""
675682
guid = getattr(item, "guid", "") or ""
683+
684+
# YAMP GUID: tv.plex.agents.custom.yamp://movie/{video_id}
676685
prefix = f"{IDENTIFIER}://movie/"
677686
if guid.startswith(prefix):
678687
return guid[len(prefix) :].rstrip("/") or None
688+
689+
# Legacy agent GUID: ...youtube-as-movies|{URL_ENCODED_PATH}|{HASH}?lang=en
690+
if guid.startswith(_LEGACY_AGENT_GUID_PREFIX):
691+
rest = guid[len(_LEGACY_AGENT_GUID_PREFIX) :]
692+
path = Path(unquote(rest.split("|")[0]))
693+
# Try extract_video_id on the filename and parent directory name.
694+
# Covers the MeTube layout where yt-dlp embeds the ID in the folder name:
695+
# "Channel/Title [VIDEO_ID]/Title.mp4"
696+
for part in (path.name, path.parent.name):
697+
video_id = extract_video_id(part)
698+
if video_id:
699+
return video_id
700+
# Fallback: stem-index lookup for no-bracket filenames where the ID
701+
# was read from info.json content during build_index.
702+
_si = stem_index if stem_index is not None else _stem_index
703+
return _si.get(path.stem)
704+
679705
return None
680706

681707

@@ -897,6 +923,7 @@ async def _sync_collection_artwork_bg(col) -> None:
897923
def _fix_all_thumbnails(
898924
meta_cache: dict[str, dict] | None = None,
899925
video_index: dict[str, str] | None = None,
926+
stem_index: dict[str, str] | None = None,
900927
) -> dict:
901928
"""Upload YAMP-proxied thumbnails for every video in YAMP-managed Plex sections.
902929
@@ -931,7 +958,7 @@ def _fix_all_thumbnails(
931958
failed += 1
932959
continue
933960
for item in items:
934-
video_id = _video_id_from_plex_item(item)
961+
video_id = _video_id_from_plex_item(item, stem_index)
935962
if not video_id:
936963
logger.warning(
937964
"_fix_all_thumbnails: skipping item with unrecognised guid %r", getattr(item, "guid", "")
@@ -970,7 +997,8 @@ async def api_fix_thumbnails():
970997
raise HTTPException(status_code=400, detail="PLEX_URL and PLEX_TOKEN env vars not set")
971998
cache = _video_meta_cache # capture refs before thread dispatch
972999
index = _video_index
973-
result = await asyncio.to_thread(_fix_all_thumbnails, cache, index)
1000+
si = _stem_index
1001+
result = await asyncio.to_thread(_fix_all_thumbnails, cache, index, si)
9741002
if "error" in result:
9751003
raise HTTPException(status_code=502, detail=result["error"])
9761004
return result

provider/tests/test_app.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,73 @@ def test_build_index_filename_id_takes_priority(tmp_path):
8282
assert dir_id not in index
8383

8484

85+
# ── _video_id_from_plex_item ──────────────────────────────────────────────────
86+
87+
88+
def _make_item(guid: str):
89+
item = MagicMock()
90+
item.guid = guid
91+
return item
92+
93+
94+
def test_video_id_from_plex_item_yamp_guid():
95+
from app import _video_id_from_plex_item
96+
97+
item = _make_item("tv.plex.agents.custom.yamp://movie/dQw4w9WgXcQ")
98+
assert _video_id_from_plex_item(item) == "dQw4w9WgXcQ"
99+
100+
101+
def test_video_id_from_plex_item_unknown_guid_returns_none():
102+
from app import _video_id_from_plex_item
103+
104+
item = _make_item("com.plexapp.agents.imdb://tt1234567?lang=en")
105+
assert _video_id_from_plex_item(item) is None
106+
107+
108+
@pytest.mark.parametrize(
109+
"guid,stem_index,expected",
110+
[
111+
# ID embedded in parent directory name (MeTube layout: "Title [ID]/Title.mp4")
112+
(
113+
"com.plexapp.agents.youtube-as-movies://youtube-as-movies|"
114+
"%2Fdata%2FBroadcast_Special_2023%20%5B9876543210%5D%2FBroadcast_Special_2023%2Emp4"
115+
"|aabbccdd?lang=en",
116+
{},
117+
"9876543210",
118+
),
119+
# ID embedded in parent directory name (8-digit numeric)
120+
(
121+
"com.plexapp.agents.youtube-as-movies://youtube-as-movies|"
122+
"%2Fdata%2FChannel%20%5BUCabc123%5D%2FConcert_Film%20%5B12345678%5D%2FConcert_Film%2Emp4"
123+
"|aabbccdd?lang=en",
124+
{},
125+
"12345678",
126+
),
127+
# ID embedded in parent directory name (short alphanumeric)
128+
(
129+
"com.plexapp.agents.youtube-as-movies://youtube-as-movies|"
130+
"%2Fdata%2FDocumentary_Series%20%5Bab12345%5D%2FDocumentary_Series%2Emp4"
131+
"|aabbccdd?lang=en",
132+
{},
133+
"ab12345",
134+
),
135+
# No brackets anywhere — ID resolved via stem_index fallback
136+
(
137+
"com.plexapp.agents.youtube-as-movies://youtube-as-movies|"
138+
"%2Fdata%2FChannel%2FConcert_Film_-__ab12345_original%2Emp4"
139+
"|aabbccdd?lang=en",
140+
{"Concert_Film_-__ab12345_original": "ab12345"},
141+
"ab12345",
142+
),
143+
],
144+
)
145+
def test_video_id_from_plex_item_legacy_guid(guid, stem_index, expected):
146+
from app import _video_id_from_plex_item
147+
148+
item = _make_item(guid)
149+
assert _video_id_from_plex_item(item, stem_index) == expected
150+
151+
85152
# ── /api/thumbnail/{video_id} ─────────────────────────────────────────────────
86153

87154

0 commit comments

Comments
 (0)