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:
+49
-1
@@ -17,7 +17,13 @@ import {
|
||||
type ErrorLogEntry,
|
||||
type BackupExportResult,
|
||||
type BackupImportResult,
|
||||
type SystemThemeInfo
|
||||
type SystemThemeInfo,
|
||||
type Source,
|
||||
type MediaItem,
|
||||
type IndexProgress,
|
||||
type IndexSourceResult,
|
||||
type SyncResult,
|
||||
type ScheduledSyncStatus
|
||||
} from '@shared/ipc'
|
||||
|
||||
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
||||
@@ -121,6 +127,48 @@ const api = {
|
||||
const listener = (_e: IpcRendererEvent, url: string): void => cb(url)
|
||||
ipcRenderer.on(IpcChannels.externalUrl, listener)
|
||||
return () => ipcRenderer.removeListener(IpcChannels.externalUrl, listener)
|
||||
},
|
||||
|
||||
// --- Media-manager sources (Pinchflat-style; see ROADMAP-PINCHFLAT.md) ---
|
||||
|
||||
listSources: (): Promise<Source[]> => ipcRenderer.invoke(IpcChannels.sourcesList),
|
||||
|
||||
/** Index (or re-index) a channel/playlist URL into the persisted source list. */
|
||||
indexSource: (url: string): Promise<IndexSourceResult> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceIndex, url),
|
||||
|
||||
/** Re-index an existing source by id (refreshes its media-item list). */
|
||||
reindexSource: (id: string): Promise<IndexSourceResult> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceReindex, id),
|
||||
|
||||
removeSource: (id: string): Promise<Source[]> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceRemove, id),
|
||||
|
||||
listSourceItems: (sourceId: string): Promise<MediaItem[]> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceItems, sourceId),
|
||||
|
||||
/** Persist that a media item has finished downloading (drives incremental sync). */
|
||||
markSourceItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath),
|
||||
|
||||
/** Toggle whether a source is watched for new uploads. */
|
||||
setSourceWatched: (id: string, watched: boolean): Promise<Source[]> =>
|
||||
ipcRenderer.invoke(IpcChannels.sourceSetWatched, id, watched),
|
||||
|
||||
/** Re-index all watched sources; resolves with the videos found new. */
|
||||
syncSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),
|
||||
|
||||
/** Read / write the Windows daily-sync scheduled task. */
|
||||
getScheduledSync: (): Promise<ScheduledSyncStatus> =>
|
||||
ipcRenderer.invoke(IpcChannels.scheduledSyncGet),
|
||||
setScheduledSync: (enabled: boolean): Promise<ScheduledSyncStatus> =>
|
||||
ipcRenderer.invoke(IpcChannels.scheduledSyncSet, enabled),
|
||||
|
||||
/** Subscribe to live indexing progress. Returns an unsubscribe function. */
|
||||
onIndexProgress: (cb: (p: IndexProgress) => void): (() => void) => {
|
||||
const listener = (_e: IpcRendererEvent, p: IndexProgress): void => cb(p)
|
||||
ipcRenderer.on(IpcChannels.indexProgress, listener)
|
||||
return () => ipcRenderer.removeListener(IpcChannels.indexProgress, listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user