diff --git a/docs/superpowers/plans/2026-08-11-4k-support.md b/docs/superpowers/plans/2026-08-11-4k-support.md new file mode 100644 index 0000000..db11a74 --- /dev/null +++ b/docs/superpowers/plans/2026-08-11-4k-support.md @@ -0,0 +1,225 @@ +# 4K Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add 2160p (4K) and 1440p (2K) as selectable download quality tiers, and update marketing copy that currently caps the advertised resolution at 1080p. + +**Architecture:** The download pipeline already resolves quality generically — the probe (`src/lib/ytdlp-probe.ts`) filters a fixed `QUALITIES` list against the source video's max height, and the yt-dlp arg builder (`src/lib/ytdlp.ts`) turns any quality string into a `height<=?N` format selector. No pipeline logic changes; only the `QUALITIES` list, its test coverage, and static marketing copy change. + +**Tech Stack:** TypeScript, Jest, next-intl (`messages/*.json`). + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-08-11-4k-support-design.md` +- Add exactly two new tiers: `2160p` and `1440p` (no other resolutions). +- No changes to `src/lib/ytdlp.ts`, `worker/processor.ts`, or the probe's filtering logic — both are already generic over the quality string. +- Marketing copy change is a literal `1080p` → `4K` swap; wording around it stays otherwise identical. +- No changes to `src/lib/downloader-platforms.ts` — it carries no resolution claim. + +--- + +### Task 1: Add 2160p/1440p to the quality list and probe test coverage + +**Files:** +- Modify: `src/lib/ytdlp-options.ts:2` +- Modify: `src/lib/__tests__/ytdlp-probe.test.ts` (add a new test in the `parseProbeOutput` describe block, after the existing "detects a video with multiple qualities" test at line 40) + +**Interfaces:** +- Consumes: nothing new. +- Produces: `QUALITIES` (from `src/lib/ytdlp-options.ts`) now includes `'2160p'` and `'1440p'`, ordered highest-to-lowest: `['best', '2160p', '1440p', '1080p', '720p', '480p', '360p']`. `ytdlp-probe.ts` and `SubmitForm.tsx` both import `QUALITIES` by name (unchanged) and iterate it in array order — later tasks and existing consumers see the two new entries automatically. + +- [ ] **Step 1: Write the failing test** + +Add this test to `src/lib/__tests__/ytdlp-probe.test.ts`, right after the `it('detects a video with multiple qualities and subtitles', ...)` block (after line 40): + +```ts + it('includes 4K and 2K when the source video offers them', () => { + const result = parseProbeOutput(json({ + formats: [ + { height: 1080, vcodec: 'avc1' }, + { height: 1440, vcodec: 'avc1' }, + { height: 2160, vcodec: 'avc1' }, + ], + })) + expect(result.availableQualities).toEqual(['best', '2160p', '1440p', '1080p']) + }) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm test -- ytdlp-probe.test.ts` +Expected: FAIL — `availableQualities` is `['best', '1080p']` because `QUALITIES` does not yet contain `'2160p'`/`'1440p'`. + +- [ ] **Step 3: Write minimal implementation** + +In `src/lib/ytdlp-options.ts`, replace line 2: + +```ts +export const QUALITIES = ['best', '2160p', '1440p', '1080p', '720p', '480p', '360p'] as const +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm test -- ytdlp-probe.test.ts` +Expected: PASS — all tests in the file pass, including the existing 1080p-source case (`'caps available qualities to the max height found'` and `'detects a video with multiple qualities and subtitles'`), which stay unaffected since `2160p`/`1440p` are correctly filtered out when `maxHeight` is below them. + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/ytdlp-options.ts src/lib/__tests__/ytdlp-probe.test.ts +git commit -m "feat: add 4K and 2K quality tiers" +``` + +--- + +### Task 2: Cover the 2160p format selector in the yt-dlp arg builder tests + +**Files:** +- Modify: `src/lib/__tests__/ytdlp.test.ts` (add a new test after the existing `it('adds a height filter to -f when quality is not "best"', ...)` block at line 41) + +**Interfaces:** +- Consumes: `buildYtdlpArgs` from `src/lib/ytdlp.ts` (unchanged signature: `(params: YtdlpParams) => string[]`), `base` fixture object defined at the top of the test file. +- Produces: nothing new — this task only adds test coverage confirming `buildYtdlpArgs` already handles `quality: '2160p'` correctly (via its existing generic `quality.replace('p', '')` logic). + +- [ ] **Step 1: Write the failing-if-broken test** + +Add this test to `src/lib/__tests__/ytdlp.test.ts`, right after the `it('adds a height filter to -f when quality is not "best"', ...)` block (after line 41): + +```ts + it('adds a height filter to -f for 4K quality', () => { + const args = buildYtdlpArgs({ ...base, quality: '2160p' }) + const idx = args.indexOf('-f') + expect(idx).toBeGreaterThan(-1) + expect(args[idx + 1]).toContain('height<=?2160') + }) +``` + +- [ ] **Step 2: Run test to verify it passes** + +Run: `npm test -- ytdlp.test.ts` +Expected: PASS immediately — `buildYtdlpArgs` already builds the filter generically from the `quality` string, so no source change is needed. This step confirms that generic behavior explicitly with a regression test. + +- [ ] **Step 3: Commit** + +```bash +git add src/lib/__tests__/ytdlp.test.ts +git commit -m "test: cover 4K format selector in buildYtdlpArgs" +``` + +--- + +### Task 3: Update marketing copy in all 4 locales to advertise 4K + +**Files:** +- Modify: `messages/en.json` (lines 52, 60, 79, 117, 166) +- Modify: `messages/fr.json` (lines 52, 60, 79, 117, 166) +- Modify: `messages/es.json` (lines 52, 60, 79, 117, 166) +- Modify: `messages/it.json` (lines 52, 60, 79, 117, 166) + +**Interfaces:** +- Consumes: nothing — these are static i18n strings rendered by `HowItWorksSection.tsx`, `FaqAccordion.tsx`, and the platform FAQ pages (via next-intl), none of which need changes since only string content changes, not keys. +- Produces: nothing consumed by later tasks. + +Each file has exactly 5 occurrences of the literal substring `1080p`, all of which should become `4K`, at these keys: +- `home.formatsDesc` +- `howItWorks.step2Desc` +- `faq.a4` +- `downloaderPages.youtube.faqA2` +- `downloaderPages.vimeo.faqA2` + +- [ ] **Step 1: Update `messages/en.json`** + +Current (lines 52, 60, 79, 117, 166): +``` + "formatsDesc": "mp4, mp3, webm, mkv -- up to 1080p" + "step2Desc": "Pick MP4, MP3, WebM or MKV and the quality you want, up to 1080p", + "a4": "Video in MP4, WebM or MKV, or audio-only in MP3, up to 1080p depending on the source video.", + "faqA2": "Up to 1080p, depending on the resolutions available for that specific video." + "faqA2": "It depends on what the uploader made available, up to 1080p in most cases." +``` + +Replace every `1080p` with `4K` (5 occurrences in this file): +``` + "formatsDesc": "mp4, mp3, webm, mkv -- up to 4K" + "step2Desc": "Pick MP4, MP3, WebM or MKV and the quality you want, up to 4K", + "a4": "Video in MP4, WebM or MKV, or audio-only in MP3, up to 4K depending on the source video.", + "faqA2": "Up to 4K, depending on the resolutions available for that specific video." + "faqA2": "It depends on what the uploader made available, up to 4K in most cases." +``` + +- [ ] **Step 2: Update `messages/fr.json`** + +Current (lines 52, 60, 79, 117, 166): +``` + "formatsDesc": "mp4, mp3, webm, mkv — jusqu'en 1080p" + "step2Desc": "Sélectionnez MP4, MP3, WebM ou MKV et la qualité souhaitée, jusqu'en 1080p", + "a4": "Vidéo en MP4, WebM ou MKV, ou audio seul en MP3, jusqu'en 1080p selon la vidéo source.", + "faqA2": "Jusqu'à 1080p, selon les résolutions disponibles pour cette vidéo en particulier." + "faqA2": "Cela dépend de ce que l'auteur a mis à disposition, jusqu'à 1080p dans la plupart des cas." +``` + +Replace every `1080p` with `4K` (5 occurrences in this file): +``` + "formatsDesc": "mp4, mp3, webm, mkv — jusqu'en 4K" + "step2Desc": "Sélectionnez MP4, MP3, WebM ou MKV et la qualité souhaitée, jusqu'en 4K", + "a4": "Vidéo en MP4, WebM ou MKV, ou audio seul en MP3, jusqu'en 4K selon la vidéo source.", + "faqA2": "Jusqu'à 4K, selon les résolutions disponibles pour cette vidéo en particulier." + "faqA2": "Cela dépend de ce que l'auteur a mis à disposition, jusqu'à 4K dans la plupart des cas." +``` + +- [ ] **Step 3: Update `messages/es.json`** + +Current (lines 52, 60, 79, 117, 166): +``` + "formatsDesc": "mp4, mp3, webm, mkv — hasta 1080p" + "step2Desc": "Selecciona MP4, MP3, WebM o MKV y la calidad deseada, hasta 1080p", + "a4": "Vídeo en MP4, WebM o MKV, o solo audio en MP3, hasta 1080p según el vídeo de origen.", + "faqA2": "Hasta 1080p, según las resoluciones disponibles para ese vídeo en concreto." + "faqA2": "Depende de lo que haya puesto a disposición quien lo subió, hasta 1080p en la mayoría de los casos." +``` + +Replace every `1080p` with `4K` (5 occurrences in this file): +``` + "formatsDesc": "mp4, mp3, webm, mkv — hasta 4K" + "step2Desc": "Selecciona MP4, MP3, WebM o MKV y la calidad deseada, hasta 4K", + "a4": "Vídeo en MP4, WebM o MKV, o solo audio en MP3, hasta 4K según el vídeo de origen.", + "faqA2": "Hasta 4K, según las resoluciones disponibles para ese vídeo en concreto." + "faqA2": "Depende de lo que haya puesto a disposición quien lo subió, hasta 4K en la mayoría de los casos." +``` + +- [ ] **Step 4: Update `messages/it.json`** + +Current (lines 52, 60, 79, 117, 166): +``` + "formatsDesc": "mp4, mp3, webm, mkv — fino a 1080p" + "step2Desc": "Seleziona MP4, MP3, WebM o MKV e la qualità desiderata, fino a 1080p", + "a4": "Video in MP4, WebM o MKV, oppure solo audio in MP3, fino a 1080p a seconda del video originale.", + "faqA2": "Fino a 1080p, in base alle risoluzioni disponibili per quel video specifico." + "faqA2": "Dipende da cosa ha reso disponibile chi ha caricato il video, fino a 1080p nella maggior parte dei casi." +``` + +Replace every `1080p` with `4K` (5 occurrences in this file): +``` + "formatsDesc": "mp4, mp3, webm, mkv — fino a 4K" + "step2Desc": "Seleziona MP4, MP3, WebM o MKV e la qualità desiderata, fino a 4K", + "a4": "Video in MP4, WebM o MKV, oppure solo audio in MP3, fino a 4K a seconda del video originale.", + "faqA2": "Fino a 4K, in base alle risoluzioni disponibili per quel video specifico." + "faqA2": "Dipende da cosa ha reso disponibile chi ha caricato il video, fino a 4K nella maggior parte dei casi." +``` + +- [ ] **Step 5: Verify no stray `1080p` remains in any locale file** + +Run: `grep -rn "1080" messages/` +Expected: no output (empty result). + +- [ ] **Step 6: Run the full test suite** + +Run: `npm test` +Expected: PASS — locale copy changes don't affect any test, and Tasks 1–2's new tests still pass. + +- [ ] **Step 7: Commit** + +```bash +git add messages/en.json messages/fr.json messages/es.json messages/it.json +git commit -m "docs: advertise 4K support in marketing copy across all locales" +```