From 87cb667848c1885acab1109d74c2c868876ef43f Mon Sep 17 00:00:00 2001 From: jlightner Date: Fri, 3 Apr 2026 23:42:43 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Added=20GET=20/videos/{video=5Fid}=20an?= =?UTF-8?q?d=20GET=20/videos/{video=5Fid}/transcrip=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "backend/routers/videos.py" - "backend/schemas.py" - "backend/tests/test_video_detail.py" GSD-Task: S01/T01 --- .gsd/completed-units-M019.json | 1 + .gsd/milestones/M020/slices/S01/S01-PLAN.md | 167 +- .../M020/slices/S01/S01-RESEARCH.md | 127 + .../M020/slices/S01/tasks/T01-PLAN.md | 69 + .../M020/slices/S01/tasks/T01-SUMMARY.md | 79 + .../M020/slices/S01/tasks/T02-PLAN.md | 79 + .../M020/slices/S01/tasks/T03-PLAN.md | 93 + .gsd/reports/M019-2026-04-03T23-30-16.html | 13386 ++++++++++++++++ .gsd/reports/index.html | 63 +- .gsd/reports/reports.json | 16 + backend/routers/videos.py | 64 +- backend/schemas.py | 14 + backend/tests/test_video_detail.py | 133 + 13 files changed, 14269 insertions(+), 22 deletions(-) create mode 100644 .gsd/completed-units-M019.json create mode 100644 .gsd/milestones/M020/slices/S01/S01-RESEARCH.md create mode 100644 .gsd/milestones/M020/slices/S01/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M020/slices/S01/tasks/T01-SUMMARY.md create mode 100644 .gsd/milestones/M020/slices/S01/tasks/T02-PLAN.md create mode 100644 .gsd/milestones/M020/slices/S01/tasks/T03-PLAN.md create mode 100644 .gsd/reports/M019-2026-04-03T23-30-16.html create mode 100644 backend/tests/test_video_detail.py diff --git a/.gsd/completed-units-M019.json b/.gsd/completed-units-M019.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/.gsd/completed-units-M019.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/.gsd/milestones/M020/slices/S01/S01-PLAN.md b/.gsd/milestones/M020/slices/S01/S01-PLAN.md index 3f96a2b..2bba271 100644 --- a/.gsd/milestones/M020/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M020/slices/S01/S01-PLAN.md @@ -1,6 +1,171 @@ # S01: [A] Web Media Player MVP -**Goal:** Build a custom media player for educational video content with HLS, speed controls, and transcript sync +**Goal:** Custom video player page with HLS playback, speed controls (0.5x–2x), and synchronized transcript sidebar, accessible from technique page key moment timestamps. **Demo:** After this: Custom video player with HLS playback, speed controls (0.5x-2x), and synchronized transcript sidebar ## Tasks +- [x] **T01: Added GET /videos/{video_id} and GET /videos/{video_id}/transcript endpoints with creator info, ordered segments, and 5 integration tests** — Add two new endpoints to the existing videos router: + +1. `GET /videos/{video_id}` — single video detail with eager-loaded creator (name, slug). Returns `SourceVideoDetail` schema (extends SourceVideoRead with creator_name, creator_slug fields). + +2. `GET /videos/{video_id}/transcript` — returns all TranscriptSegments for a video, ordered by segment_index. Returns `TranscriptForPlayerResponse` with video_id and segments list. + +Both return 404 for non-existent video IDs. + +## Steps + +1. Read `backend/schemas.py` to understand existing SourceVideoRead and TranscriptSegmentRead schemas. +2. Add new schemas to `backend/schemas.py`: + - `SourceVideoDetail` — extends SourceVideoRead, adds `creator_name: str`, `creator_slug: str`, `video_url: str | None = None` (always None for now) + - `TranscriptForPlayerResponse` — `video_id: uuid.UUID`, `segments: list[TranscriptSegmentRead]`, `total: int` +3. Read `backend/routers/videos.py` to understand the existing list endpoint pattern. +4. Add `GET /videos/{video_id}` endpoint to `backend/routers/videos.py`: + - Query SourceVideo by ID with `selectinload(SourceVideo.creator)` + - Return 404 if not found + - Map to SourceVideoDetail with creator_name and creator_slug from the joined creator +5. Add `GET /videos/{video_id}/transcript` endpoint: + - Query TranscriptSegments where source_video_id matches, ordered by segment_index + - Return 404 if video doesn't exist + - Return TranscriptForPlayerResponse +6. Write `backend/tests/test_video_detail.py` with tests: + - test_get_video_detail_success — creates a video+creator, verifies response shape + - test_get_video_detail_404 — random UUID returns 404 + - test_get_transcript_success — creates video + segments, verifies ordering and count + - test_get_transcript_404 — random UUID returns 404 + - test_get_transcript_empty — video exists but has no segments, returns empty list +7. Run tests and fix any issues. + +## Must-Haves + +- [ ] GET /videos/{video_id} returns video detail with creator_name and creator_slug +- [ ] GET /videos/{video_id}/transcript returns segments ordered by segment_index +- [ ] Both endpoints return 404 for non-existent video IDs +- [ ] Tests pass + +## Failure Modes + +| Dependency | On error | On timeout | On malformed response | +|------------|----------|-----------|----------------------| +| PostgreSQL | 500 with logged traceback | FastAPI default timeout | N/A (ORM-mapped) | +| video_id UUID parse | FastAPI returns 422 automatically | N/A | N/A | + - Estimate: 45m + - Files: backend/routers/videos.py, backend/schemas.py, backend/tests/test_video_detail.py + - Verify: cd backend && python -m pytest tests/test_video_detail.py -v +- [ ] **T02: Build VideoPlayer component with HLS, custom controls, and media sync hook** — Build the core video player infrastructure: hls.js integration, custom playback controls, and the useMediaSync hook that shares playback state between player and transcript. + +## Steps + +1. Install hls.js: `cd frontend && npm install hls.js` +2. Create `frontend/src/hooks/useMediaSync.ts`: + - Custom hook managing shared playback state: `currentTime`, `duration`, `isPlaying`, `playbackRate`, `videoRef` + - Listens to `timeupdate` (fires ~4Hz), `play`, `pause`, `ratechange`, `loadedmetadata` events on the video element + - Exposes `seekTo(time: number)`, `setPlaybackRate(rate: number)`, `togglePlay()` actions + - Returns `{ currentTime, duration, isPlaying, playbackRate, videoRef, seekTo, setPlaybackRate, togglePlay }` +3. Create `frontend/src/components/VideoPlayer.tsx`: + - Accepts props: `src: string | null`, `startTime?: number`, `mediaSync: ReturnType` + - If `src` is null, render a styled "Video not available" placeholder with explanation text + - If src ends in `.m3u8` or device doesn't support native HLS: lazy-load hls.js via dynamic `import('hls.js')`, call `hls.loadSource(src)`, `hls.attachMedia(videoEl)` + - If native HLS supported (Safari): set `video.src = src` directly + - For `.mp4` URLs: use native `