fix(e5): address code-review findings (TMDB guid media_type, search ingest guard, discover keying)
Follow-ups from the epic-end /code-review of the E5 branch: - tmdb.resolve_recs + people._resolve_known_for: match recommendations/ credits to the library bucketed by media_type. TMDB movie ids and tv ids are independent sequences, so a bare `tmdb:<id>` match could deep-link a movie rec to an unrelated show (or vice-versa) on a numeric collision. Regression test added (test_resolve_recs_matches_by_media_type_not_bare_id). - routes/search: wrap the _ingest_candidates call so a DB/enrich failure degrades to a 422 with a message instead of a bare 500 hidden behind the FE's generic "search failed" (extends the B-core-1 rationale); rollback on failure. Stamp enriched_at with its own timestamp taken after the enrich, not the stub-insert placeholder. - routes/plex/discover: key serialized cards back by rating_key (globally unique in Plex) instead of id(obj); fix the stale rows 3-tuple annotation.
This commit is contained in:
@@ -12,6 +12,7 @@ Ingested results are flagged (videos.via_search / channels.from_search) so the L
|
||||
this search-discovered "noise" by default. Shorts and currently-live/upcoming videos are never
|
||||
ingested this way (Shorts are confirmed by the youtube.com/shorts probe; live/upcoming are
|
||||
dropped from the search response up front)."""
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -43,6 +44,8 @@ from app.youtube import search_scrape
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
from app.youtube.shorts import make_client, probe_is_short
|
||||
|
||||
log = logging.getLogger("siftlode.search")
|
||||
|
||||
router = APIRouter(prefix="/api/search", tags=["search"])
|
||||
|
||||
# search.list costs this many quota units per page.
|
||||
@@ -162,12 +165,13 @@ def _ingest_candidates(
|
||||
detail_by_id = {it["id"]: it for it in yt.get_videos(ids)}
|
||||
except YouTubeError:
|
||||
detail_by_id = {}
|
||||
enriched_at = datetime.now(timezone.utc) # when THIS enrich ran (distinct from the stub placeholder)
|
||||
for v in videos:
|
||||
item = detail_by_id.get(v.id)
|
||||
if item is None:
|
||||
continue
|
||||
apply_video_details(v, item)
|
||||
v.enriched_at = now
|
||||
v.enriched_at = enriched_at
|
||||
db.commit()
|
||||
|
||||
# 4) Confirm/deny Shorts (no quota) so none enter via search.
|
||||
@@ -285,7 +289,19 @@ def search_youtube(
|
||||
and it["channel_id"]
|
||||
and it["channel_id"] not in blocked
|
||||
]
|
||||
collected.extend(_ingest_candidates(db, yt, user, candidates))
|
||||
# Ingest (DB writes + enrich) runs outside the page-fetch try above; guard it too so a
|
||||
# write/shape failure degrades to a 422 with a message on this JSON endpoint instead of a
|
||||
# bare 500 hidden behind the FE's generic "search failed" (same rationale as B-core-1).
|
||||
try:
|
||||
collected.extend(_ingest_candidates(db, yt, user, candidates))
|
||||
except Exception as exc: # noqa: BLE001 — any ingest failure, not just YouTubeError
|
||||
db.rollback()
|
||||
log.exception("live search ingest failed for %r", term)
|
||||
if collected:
|
||||
break # keep what earlier pages already gathered
|
||||
raise HTTPException(
|
||||
status_code=422, detail=f"YouTube search failed: {exc}"
|
||||
) from exc
|
||||
next_cursor = page["next_page_token"]
|
||||
# The API source pages cost 100 units each — never auto-page; one page per request.
|
||||
if source == "api" or len(collected) >= limit or not next_cursor:
|
||||
|
||||
Reference in New Issue
Block a user