Add Pinchflat-style media manager: index channels into playlist folders

Index an entire YouTube channel or playlist once, then download it into
organized <Channel>/<Playlist>/<NNN> - <Title> folders, with incremental
re-sync. Built in six rebuild-gated phases (F-K); see ROADMAP-PINCHFLAT.md.

- F: channel-walk indexer (/playlists + /videos) -> persisted Source +
  MediaItem JSON stores; pure classify/dedup logic in indexerCore.ts
- G: Windows-safe folder paths + dir sanitizer (collectionOutputTemplate)
- H: Library tab + source tree + "Download pending" into the existing queue
- I: state-preserving re-index merge + persist-downloaded-on-complete
- J: watched sources, RSS fast-check, Task Scheduler + --sync launch
  (the OS-level scheduling/RSS need a real-install smoke test)
- K: .info.json / thumbnail / .description sidecars for media servers

Also gitignore .gitea-token. 106 unit tests pass; typecheck + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 20:50:13 -04:00
parent 81b97ea429
commit 47da3e6746
26 changed files with 2529 additions and 46 deletions
+13 -3
View File
@@ -7,7 +7,12 @@ import { getSettings, getDownloadArchivePath } from './settings'
import { getCookiesFilePath } from './cookies'
import { listTemplates } from './templates'
import { assertHttpUrl } from './url'
import { buildArgs, parseExtraArgs, formatCommandLine } from './buildArgs'
import {
buildArgs,
parseExtraArgs,
formatCommandLine,
collectionOutputTemplate
} from './buildArgs'
import { cleanError } from './log'
import { addErrorLog } from './errorlog'
import {
@@ -161,7 +166,12 @@ function resolveExtraArgs(opts: StartDownloadOptions, settings: Settings): strin
export function buildCommand(opts: StartDownloadOptions): string[] {
const settings = getSettings()
const outDir = opts.outputDir?.trim() || settings.outputDir || app.getPath('downloads')
const template = settings.filenameTemplate?.trim() || '%(title)s.%(ext)s'
// A collection (media-manager) download is filed into <channel>/<playlist>/
// <NNN> - <title> folders; an ordinary download uses the flat filenameTemplate.
const filenameTemplate = settings.filenameTemplate?.trim() || '%(title)s.%(ext)s'
const outputTemplate = opts.collection
? collectionOutputTemplate(outDir, opts.collection, '%(title)s.%(ext)s')
: join(outDir, filenameTemplate)
// Per-download override wins; otherwise use the persisted defaults.
const options = opts.options ?? settings.downloadOptions
// Silently fall back to yt-dlp's own downloader if aria2c.exe wasn't dropped
@@ -183,7 +193,7 @@ export function buildCommand(opts: StartDownloadOptions): string[] {
downloadArchivePath: settings.downloadArchive ? getDownloadArchivePath() : undefined
}
const extraArgs = resolveExtraArgs(opts, settings)
return buildArgs(opts, join(outDir, template), options, getBinDir(), access, extraArgs)
return buildArgs(opts, outputTemplate, options, getBinDir(), access, extraArgs)
}
/** Build the exact command line for the current form state, without running it. */