1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
# tasks.md — live progress tracker
> Updated at each milestone. What's done, in-flight, blocked. The orchestrator
> reads this to know the project state; summoned agents update their rows.
## Done
- [x] **Research** — `.research/` (8 docs): stack decision (Bun+TS, shell out to
yt-dlp), chapters, SponsorBlock, ffmpeg, tagging, project structure.
- [x] **Scaffold backend** — Bun + TS monorepo: `contract`, `job-store` (pure
reducer + tests), `downloader` (arg builders + tests), `host-bin` (Bun.serve).
typecheck/test/check green.
- [x] **Scaffold frontend** — Vite + Svelte 5 + Tailwind/DaisyUI. Consumes
`@yt-music/contract` as `file:` dep. typecheck/check/build green.
- [x] **bin/up** — full stack (backend :24303 + frontend :24304), Tailscale-ready,
Vite proxy for /api, /ws, /health.
- [x] **Governance** — AGENTS.md (root + backend + web), GLOSSARY.md,
ORCHESTRATOR.md, tasks.md. Orchestrator role + worktree workflow + core
principles written in.
- [x] **Phase 1: single-track download** — end-to-end vertical slice:
`downloader` (real Bun.spawn yt-dlp: getInfo + download + progress parsing,
13 tests), `storage-sqlite` (bun:sqlite, append-only events, reconcile
recovery, 5 tests), `host-bin` (POST /api/jobs, GET /api/jobs/:id, WS /ws
with subscribe + event replay, binary resolution), `contract` (API +
WS protocol types), frontend (download form + live progress over WS,
pure download reducer, 7 tests). Verified: downloaded a real MP3 (7.4 MB,
properly tagged) end-to-end.
- [x] **Phase 2: tagging + library (backend)** — `tagger` (pure core:
detectFormat/toTags/buildId3Tags; shell: createTagReader via music-metadata,
createTagWriter via node-id3/ID3v2 MP3; 12 tests) + `library` (pure core:
trackId/toTrack/isAudioFile; shell: createLibrary scans output dir, indexes
via injected TagReader, listTracks(): Track[]; 9 tests) + `host-bin`
GET /api/library → { tracks: Track[] }. typecheck/test/check green (39
tests). Verified end-to-end via curl against real output/ MP3s. Frontend
library browse handed off to the web agent.
- [x] **Phase 3: album splitting (chapters)** — `cut-plan` (PURE core:
parseChapterTitle, computeKeepRanges, computeDefaultDraft, finalizeCutPlan
+ CutPlanError validation; 40 tests), `sponsorblock` (thin fetch client:
hash-prefix API, injectable fetch, optional/graceful; 15 tests), `cutter`
(ffmpeg executor: FLAC atrim-re-encode [lossless + correct duration] /
MP3 stream-copy + concat demuxer; pure arg builders + 5 real-ffmpeg
integration tests; 18 tests), `host-bin` split-by-chapters flow
(getInfo → draft → await confirm → download raw → finalize → cut → tag →
done) + POST /api/jobs/:id/confirm. downloader bug fix (yt-dlp writes
Destination lines to STDOUT, not stderr — was silently broken since Phase
1). typecheck/test/check green (116 tests). Verified end-to-end against
https://youtu.be/7yzGBaiAMfw: 7 chapters → 7 tagged MP3s with correct
durations, both packages-direct and real HTTP+WS+confirm host E2E.
Frontend segment editor handed off to the web agent.
- [x] **Phase 4: SponsorBlock integration (backend verified)** — the
sponsorblock → cut-plan → cutter pipeline was already wired in Phase 3;
this phase verified it end-to-end + hardened edge-case tests. Verified:
host-bin `runSplitJob` calls `sponsorblock.getSegments()` (graceful 404 →
empty draft), `computeDefaultDraft` includes sponsors as toggleable
`removedSegments` (enabled by default, labeled), `finalizeCutPlan` computes
correct `keepRanges` (excluding enabled segments), the cutter extracts +
concats each range (lossless), and `{type:"cutting"}` progress events fire
per segment. Added 11 pure-core tests to cut-plan (40 → 51): touching
adjacent segments, whole-chapter coverage, zero-length segments, unsorted
input, boundary-at-start/end, end-to-end draft→finalize with real SB data,
boundary-spanning sponsors, disabled toggles, default-category matrix.
Tested against real SponsorBlock API (7yzGBaiAMfw=no data, JGwNGJdvx8/
9bZkp7q19f0=sponsor data) + real host E2E (7 chapters → 7 MP3s, 7 cutting
progress events). typecheck/test/test:bun/check green (127 vitest + 5 bun
tests). Frontend handed off to conversation 54bf6fde (FE4).
- [x] **Phase 5: Organize (settings + path templates + move/rename)** —
settings persistence, a pure path-template engine, and library
move/rename. NEW package `path-template` (PURE core: `renderPathTemplate`,
`sanitizePathComponent`, `padTrackNumber`; 28 tests covering empty
fields, fallbacks, special-char sanitization, unicode, and path-traversal
defense). `contract` gained `track?` on `Track` + `UpdateTrackRequest` /
`UpdateTrackResponse` / `SaveSettingsRequest` / `OrganizeResponse`.
`tagger` gained `track` (read via music-metadata `common.track.no`, write
via node-id3 TRCK). `storage-sqlite` gained a single-row `settings` table
+ `getSettings()` (persisted-or-default) / `saveSettings()` (4 new bun
tests). `library` gained `applyPathTemplate()` (read tags → render → move)
+ `renameTrack()` (merge tags → write → move → new Track), with a
`writer?` injection (8 new integration tests against a real temp dir).
`host-bin` wires `GET/PUT /api/settings`, `PATCH /api/library/:id`
(rename), `POST /api/library/organize` (bulk move), and applies the path
template after every download (single + split). Verified end-to-end: a
real tagged MP3 PATCH-renamed to `{artist}/{album}/{track} - {title}.{ext}`,
file moved on disk, new path-derived id returned. typecheck/test/test:bun/
check green (169 vitest + 9 bun tests). Frontend handed off to conversation
322772a1 (FE5).
- [x] **Phase 6: FLAC tagging** — `tagger` now WRITES FLAC, not just MP3. New
pure core `buildMetaflacArgs(tags, filePath) → string[]` (ZERO I/O; builds
the metaflac argv: `--remove-all-tags` → `--set-tag FIELD=VALUE` for
non-empty title/artist/album → `TRACKNUMBER=N` when track>0 → optional
`--import-picture-from <artPath>` → file path). Each argv token is one raw
element (execFile, NOT a shell) so `;`/`'`/`"`/newlines/`=`/unicode pass
verbatim (verified against metaflac 1.5.0). New `MetaflacTags` type (Tags
minus duration/format + optional artPath) + `resolveMetaflac` (explicit →
env → guarded `Bun.which` → 'metaflac'). `createTagWriter(metaflacBin?)`
now branches on detectFormat: mp3→node-id3 (unchanged), flac→metaflac via
`node:child_process` execFile (works under Node/vitest + Bun, same as the
cutter — NOT Bun.spawn). 17 pure unit tests + 6 integration tests against
real metaflac+ffmpeg+music-metadata (write→read round-trip, overwrite,
empty fields, special chars, MP3 regression, unsupported-format error).
host-bin passes `YTMDL_METAFLAC_PATH` to both writers + removed the
'best-effort; FLAC writes deferred' comment — the split flow now tags FLAC
output correctly. No contract change (Track/Tags already had `track`).
typecheck/test/test:bun/check green (192 vitest + 9 bun tests).
## Next
(none — all seven phases complete)
- [x] **Phase 7: MusicBrainz text search (backend)** — NEW package
`@yt-music/musicbrainz`: thin HTTP client for the MB web service API.
PURE core (zero I/O): `buildRecordingSearchUrl` /
`buildReleaseSearchUrl` / `buildReleaseLookupUrl` (Lucene query folding,
URL encoding), `parseRecordingSearch` / `parseReleaseSearch` /
`parseReleaseDetail` (media[].tracks[] → flat track list with GLOBAL
running position for multi-disc), `matchAlbumToDraft` (exact-count →
positional 1:1; mismatched → greedy normalized Levenshtein title match,
no duplicate track assignments; confidence position/title/none) +
`normalizeTitle` / `levenshtein` / `titleSimilarity` helpers. INJECTED
shell `createMusicBrainzClient(opts)`: injectable fetch + baseUrl + UA,
serialized promise-chain rate-limited to 1 req/sec (MB's hard limit). 45
tests with canned JSON fixtures (no network). `contract` gained
MetadataSearchRequest/MetadataResult/ReleaseTrack/ReleaseDetail/
AlbumMatch/AlbumMatchResult/MatchAlbumRequest. `host-bin` wires
`createMusicBrainzClient()` + 3 routes: POST /api/metadata/search,
GET /api/metadata/release/:mbid, POST /api/metadata/match-album (fetches
release → pure matchAlbumToDraft). typecheck/test/test:bun/check green
(237 vitest + 9 bun tests). Frontend handed off to conversation
453ec154 (FE7).
|