feat(recs): /api/videos/:id/related — same-channel + FTS + trending fill#65
Conversation
…ding fill Replaces the /trending fallback the Watch up-next list used to use. The new endpoint walks three passes (same channel by recency, FTS5 over the source title, trending top-up), deduped and capped at the requested limit. Hidden / soft-deleted / DMCA-disabled / non-ready rows are filtered at query time so takedowns disappear from related lists immediately. Cached per (id, limit) in KV with a 5-minute TTL — uploads/deletes don't bust this directly; the TTL bounds the staleness window. Closes ALO-152. https://claude.ai/code/session_01FMnY8ZmsUxSktN5PmjS9JR
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
Caution Review failedPull request was closed or merged during review WalkthroughA new "related videos" recommendation endpoint is implemented at ChangesRelated Videos Recommendation System
Sequence DiagramsequenceDiagram
participant Frontend as Watch Page
participant Worker as Related Endpoint
participant Cache as KV Cache
participant DB as D1 Database
Frontend->>Worker: GET /api/videos/:id/related?limit=12
Worker->>Cache: Check cache for (id, limit)
alt Cache Hit
Cache-->>Worker: Return cached videos
Worker->>Frontend: 200 { videos, cached: true }
else Cache Miss
Worker->>DB: Query source video (viewability check)
DB-->>Worker: Source video details
Worker->>DB: Query same-channel recent uploads
DB-->>Worker: Same-channel results
alt Limit not met
Worker->>DB: Query FTS matches by title
DB-->>Worker: FTS results
end
alt Still underfilled
Worker->>DB: Query top-viewed/recent (trending)
DB-->>Worker: Trending results
end
Worker->>Cache: Put videos with TTL (best-effort)
Worker->>Frontend: 200 { videos, cached: false }
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsThese MCP integrations need to be re-authenticated in the Integrations settings: Sentry Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b11c513a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| WHERE id = ? AND deleted_at IS NULL AND hidden_at IS NULL | ||
| AND (dmca_status IS NULL OR dmca_status != 'disabled')`, |
There was a problem hiding this comment.
Permit owner-visible hidden videos in related source lookup
The source-row predicate hard-requires hidden_at IS NULL, so /api/videos/:id/related returns 404 for a creator viewing their own hidden video even though GET /api/videos/:id explicitly allows owners to access hidden content. Since Watch.tsx now always uses /related for the sidebar, this causes a regression where owner-visible hidden videos lose Up Next/autoplay entirely. The source lookup should account for the authenticated user (or mirror the visibility rules used by the video detail route) instead of unconditionally rejecting hidden sources.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request implements a related-video recommendation system for the Watch page, replacing the previous trending fallback. The new backend endpoint uses a tiered strategy involving same-channel uploads, FTS5 title matching, and trending top-up, with results cached in KV. Review feedback suggests optimizing the frontend fetch limit to match the display count and ensuring the source video query filters for 'ready' status to maintain consistency with visibility rules.
| if (!id) return; | ||
| let cancelled = false; | ||
| void fetch('/api/videos/trending?limit=12') | ||
| void fetch(`/api/videos/${encodeURIComponent(id)}/related?limit=12`) |
There was a problem hiding this comment.
The frontend is fetching 12 related videos but only displaying 8 (as seen on line 152). Since the /related endpoint now correctly excludes the source video, you can optimize this by requesting exactly the number of videos you intend to show.
| void fetch(`/api/videos/${encodeURIComponent(id)}/related?limit=12`) | |
| void fetch(`/api/videos/${encodeURIComponent(id)}/related?limit=8`) |
| `SELECT id, user_id, title FROM videos | ||
| WHERE id = ? AND deleted_at IS NULL AND hidden_at IS NULL | ||
| AND (dmca_status IS NULL OR dmca_status != 'disabled')`, |
There was a problem hiding this comment.
The PR description mentions that 'non-ready rows are filtered at query time', but the initial query to find the source video does not check the status column. If a video is still in encoding or pending_encode status, it might be better to return a 404 or a specific error rather than attempting to find related videos for an incomplete record.
`SELECT id, user_id, title FROM videos
WHERE id = ? AND deleted_at IS NULL AND hidden_at IS NULL
AND status = 'ready'
AND (dmca_status IS NULL OR dmca_status != 'disabled')`,There was a problem hiding this comment.
Pull request overview
Adds a first-pass related-videos recommendation endpoint for the Watch “Up next” sidebar, and wires the frontend to use it instead of the /trending stopgap. The backend endpoint performs three deduped passes (same-channel recency → title FTS → global fill) and caches per (videoId, limit) in KV.
Changes:
- Introduce
GET /api/videos/:id/relatedwith KV caching and three-pass deduped selection logic. - Add a dedicated test suite covering query validation, ordering/fallthrough, filtering, and caching behavior.
- Update
Watch.tsxto fetch related videos for the up-next sidebar.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/workers/related.ts |
New related-videos API route with same-channel/FTS/fill passes and KV caching. |
src/workers/related.test.ts |
New vitest coverage for the related endpoint and caching key behavior. |
src/workers/index.ts |
Registers relatedRoutes with the worker app router. |
src/frontend/pages/Watch.tsx |
Switches up-next fetch from /trending to /api/videos/:id/related. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Hidden / soft-deleted / DMCA-disabled / non-ready rows are filtered at | ||
| // query time so a takedown disappears from related lists immediately. |
| const source = await c.env.DB.prepare( | ||
| `SELECT id, user_id, title FROM videos | ||
| WHERE id = ? AND deleted_at IS NULL AND hidden_at IS NULL | ||
| AND (dmca_status IS NULL OR dmca_status != 'disabled')`, | ||
| ) | ||
| .bind(id) | ||
| .first<{ id: string; user_id: string; title: string }>(); | ||
| if (!source) { | ||
| return c.json({ error: 'Video not found' }, 404); | ||
| } |
| `SELECT v.id, v.title, v.thumbnail_url, v.view_count, v.created_at, | ||
| u.name AS channel_name, u.username AS channel_username | ||
| FROM videos v | ||
| LEFT JOIN user u ON u.id = v.user_id | ||
| WHERE v.deleted_at IS NULL AND v.hidden_at IS NULL | ||
| AND v.status = 'ready' | ||
| AND (v.dmca_status IS NULL OR v.dmca_status != 'disabled') | ||
| AND v.id NOT IN (${placeholders(seenList.length)}) | ||
| ORDER BY v.view_count DESC, v.created_at DESC | ||
| LIMIT ?`, |
| void fetch(`/api/videos/${encodeURIComponent(id)}/related?limit=12`) | ||
| .then(async (r) => { | ||
| if (!r.ok) throw new Error('failed'); | ||
| return (await r.json()) as { videos: UpNextVideo[] }; | ||
| }) | ||
| .then((data) => { | ||
| if (cancelled) return; | ||
| const filtered = data.videos.filter((v) => v.id !== id).slice(0, 8); | ||
| setUpNext(filtered); | ||
| upNextRef.current = filtered; | ||
| const next = data.videos.slice(0, 8); | ||
| setUpNext(next); | ||
| upNextRef.current = next; |
| // Hand-rolled D1 stub keyed on SQL fragments the route actually issues. New | ||
| // queries must be added explicitly so a schema change can never silently fall | ||
| // through. | ||
| function fakeDB(videos: VideoRow[]): FakeDBResult { |
Summary
/api/videos/:id/relatedendpoint that returns related videos via three deduped passes: same channel by recency → FTS5 over the source title → trending top-up.Watch.tsxnow calls/relatedinstead of the stopgap/trendingfor the up-next sidebar.(id, limit)KV cache with a 5-minute TTL.Hidden / soft-deleted / DMCA-disabled / non-ready rows are filtered at query time so takedowns disappear from related lists immediately. Reuses
buildFtsQueryfromsearch.tsso FTS5 sanitisation stays in one place.Closes ALO-152.
Test plan
npm run lint— cleannpm run type-check— cleannpm test -- --run— 414/414 passing (+8 new tests)x-spooool-cache: hit)https://claude.ai/code/session_01FMnY8ZmsUxSktN5PmjS9JR
Generated by Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests