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:
+1
-33
@@ -4,6 +4,7 @@ import { getYtdlpPath } from './binaries'
|
||||
import { fmtBytes } from './download'
|
||||
import { cleanError } from './log'
|
||||
import { assertHttpUrl } from './url'
|
||||
import { entryUrl, fmtDuration, type RawEntry } from './indexerCore'
|
||||
import {
|
||||
BEST_FORMAT_ID,
|
||||
type ProbeResult,
|
||||
@@ -26,16 +27,6 @@ interface RawFormat {
|
||||
filesize_approx?: number
|
||||
}
|
||||
|
||||
interface RawEntry {
|
||||
id?: string
|
||||
title?: string
|
||||
url?: string
|
||||
webpage_url?: string
|
||||
duration?: number
|
||||
uploader?: string
|
||||
channel?: string
|
||||
}
|
||||
|
||||
interface RawInfo {
|
||||
_type?: string
|
||||
title?: string
|
||||
@@ -92,29 +83,6 @@ function buildInfo(data: RawInfo): MediaInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/** Seconds → 'M:SS' or 'H:MM:SS'. Flat playlist entries give duration as a number. */
|
||||
function fmtDuration(sec?: number): string | undefined {
|
||||
if (sec == null || !Number.isFinite(sec)) return undefined
|
||||
const s = Math.max(0, Math.round(sec))
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
const r = s % 60
|
||||
const mm = h ? String(m).padStart(2, '0') : String(m)
|
||||
return `${h ? `${h}:` : ''}${mm}:${String(r).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the URL we'll enqueue for a playlist entry. Prefer an explicit http(s)
|
||||
* URL; fall back to a YouTube watch URL built from the id (the common case where
|
||||
* flat entries carry only an id). Returns null when nothing usable is present.
|
||||
*/
|
||||
function entryUrl(e: RawEntry): string | null {
|
||||
const cand = e.url || e.webpage_url
|
||||
if (cand && /^https?:\/\//i.test(cand)) return cand
|
||||
if (e.id) return `https://www.youtube.com/watch?v=${e.id}`
|
||||
return null
|
||||
}
|
||||
|
||||
function buildPlaylist(data: RawInfo): PlaylistInfo {
|
||||
const entries: PlaylistEntry[] = []
|
||||
;(data.entries ?? []).forEach((e, i) => {
|
||||
|
||||
Reference in New Issue
Block a user