feat(roadmap): surface stale checkboxes + collection output-path setting

Command preview + incognito toggle were already wired end-to-end in the
DownloadBar (Show command button, Incognito checkbox) — flipped the two
stale ROADMAP.md checkboxes with notes; verified live in the preview.

Output-path setting (PINCHFLAT): new Settings.collectionOutputTemplate — a
raw yt-dlp -o template overriding the default Channel/Playlist/NNN-Title
layout for Library downloads (empty = default). collectionOutputTemplate()
takes an optional custom template (joined under outDir, left for yt-dlp to
expand); download.ts passes the setting; zod schema validates it as a safe
relative template; Settings -> Filenames field. Tests in buildArgs +
settingsSchema; live-verified the field renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:44:44 -04:00
parent 72feaa62f8
commit 42b7b2e01f
8 changed files with 98 additions and 16 deletions
+21
View File
@@ -735,6 +735,27 @@ describe('collectionOutputTemplate', () => {
)
expect(t.replace(/\\/g, '/')).toBe('C:/out/A B/Untitled/001 - %(title)s.%(ext)s')
})
it('a custom template overrides the built-in layout and is left for yt-dlp to expand', () => {
const t = collectionOutputTemplate(
'C:/out',
ctx,
'%(title)s.%(ext)s',
'%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'
)
expect(t.replace(/\\/g, '/')).toBe(
'C:/out/%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'
)
})
it('a blank/whitespace custom template falls back to the built-in layout', () => {
expect(collectionOutputTemplate('C:/o', ctx, 'f', '').replace(/\\/g, '/')).toBe(
'C:/o/DevChannel/Full Course/003 - f'
)
expect(collectionOutputTemplate('C:/o', ctx, 'f', ' ').replace(/\\/g, '/')).toBe(
'C:/o/DevChannel/Full Course/003 - f'
)
})
})
describe('metadata overrides (--replace-in-metadata)', () => {
+14
View File
@@ -85,6 +85,20 @@ describe('parseSettingsField (CC9 schema parity)', () => {
expect(parseSettingsField('filenameTemplate', '../%(title)s.%(ext)s')).toEqual(fail)
})
it('collectionOutputTemplate: empty allowed; relative multi-segment ok; traversal rejected', () => {
expect(parseSettingsField('collectionOutputTemplate', '')).toEqual(ok(''))
expect(
parseSettingsField(
'collectionOutputTemplate',
' %(uploader)s/%(playlist)s/%(title)s.%(ext)s '
)
).toEqual(ok('%(uploader)s/%(playlist)s/%(title)s.%(ext)s'))
expect(parseSettingsField('collectionOutputTemplate', '../%(title)s.%(ext)s')).toEqual(fail)
expect(parseSettingsField('collectionOutputTemplate', 'C:\\abs\\%(title)s.%(ext)s')).toEqual(
fail
)
})
it('rateLimit: yt-dlp format or empty', () => {
expect(parseSettingsField('rateLimit', '2.5M')).toEqual(ok('2.5M'))
expect(parseSettingsField('rateLimit', ' 500K ')).toEqual(ok('500K'))