Files
AeroFetch/src/shared/ipc.ts
T
debont80 da37690b42 feat: Phase N power-user surface (terminal, palette, per-item, sort, url-templates)
- Built-in yt-dlp terminal: src/main/terminal.ts streams raw yt-dlp runs
  (binary fixed, gated on customCommandEnabled), new TerminalView + sidebar tab
- Command palette (Ctrl/Cmd+K), portal-free CommandPalette.tsx wired in App
- Per-playlist-item editing: per-row video/audio toggle + All video/All audio
  batch; addPlaylist enqueues via addMany with per-item kind
- Weighted format sorting: DownloadOptions.formatSort -> raw -S (overrides codec)
- URL-regex template auto-matching: CommandTemplate.urlPattern + selectExtraArgs
  matchesUrl, threaded through resolveExtraArgs; field in TemplateManager

typecheck + test (192) + electron-vite build all clean. Roadmap: Phase N COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:10:22 -04:00

712 lines
26 KiB
TypeScript

/**
* Shared contract between main and renderer.
* IPC channel names + payload types live here so both sides stay in sync.
*/
export const IpcChannels = {
/** the AeroFetch app version (package.json / app.getVersion) */
appVersion: 'app:version',
/** check the configured Gitea repo for a newer AeroFetch release */
appUpdateCheck: 'app:update-check',
/** download the latest release's installer to a temp file */
appUpdateDownload: 'app:update-download',
/** launch a downloaded installer and quit so it can replace the app */
appUpdateRun: 'app:update-run',
/** main → renderer push channel for installer download progress */
appUpdateProgress: 'app:update-progress',
ytdlpVersion: 'ytdlp:version',
ffmpegVersion: 'ffmpeg:version',
probe: 'media:probe',
downloadStart: 'download:start',
downloadCancel: 'download:cancel',
downloadPause: 'download:pause',
defaultFolder: 'download:default-folder',
chooseFolder: 'download:choose-folder',
openPath: 'shell:open-path',
showInFolder: 'shell:show-in-folder',
clipboardRead: 'clipboard:read',
settingsGet: 'settings:get',
settingsSet: 'settings:set',
historyList: 'history:list',
historyAdd: 'history:add',
historyRemove: 'history:remove',
historyRemoveMany: 'history:remove-many',
historyClear: 'history:clear',
/** main → renderer push channel for live download events */
downloadEvent: 'download:event',
cookiesLogin: 'cookies:login',
cookiesStatus: 'cookies:status',
cookiesClear: 'cookies:clear',
templatesList: 'templates:list',
templatesSave: 'templates:save',
templatesRemove: 'templates:remove',
commandPreview: 'command:preview',
ytdlpUpdate: 'ytdlp:update',
/** main → renderer push channel for background yt-dlp auto-update status */
ytdlpAutoUpdateStatus: 'ytdlp:auto-update-status',
errorLogList: 'errorlog:list',
errorLogClear: 'errorlog:clear',
backupExport: 'backup:export',
backupImport: 'backup:import',
systemThemeGet: 'system-theme:get',
/** main → renderer push channel for OS theme/contrast changes (nativeTheme 'updated') */
systemThemeUpdate: 'system-theme:update',
openHighContrastSettings: 'shell:open-high-contrast-settings',
/** main → renderer push channel: a URL handed to AeroFetch from outside the app
* (the aerofetch:// protocol, or a .url file via Explorer's "Send to" menu) */
externalUrl: 'external-url',
// --- Sources & media-manager indexing (Pinchflat-style, see ROADMAP-PINCHFLAT.md) ---
sourcesList: 'sources:list',
sourceIndex: 'sources:index',
sourceReindex: 'sources:reindex',
sourceRemove: 'sources:remove',
sourceItems: 'sources:items',
/** mark a media item downloaded once its collection download completes */
sourceItemDownloaded: 'sources:item-downloaded',
/** toggle whether a source is watched for new uploads (Phase J) */
sourceSetWatched: 'sources:set-watched',
/** re-index all watched sources and return the videos that are new */
sourcesSync: 'sources:sync',
/** get / set the Windows scheduled-sync task (Task Scheduler) */
scheduledSyncGet: 'sources:scheduled-sync-get',
scheduledSyncSet: 'sources:scheduled-sync-set',
/** main → renderer push channel for live indexing progress */
indexProgress: 'sources:index-progress',
// --- Built-in yt-dlp terminal (Phase N) ---
terminalRun: 'terminal:run',
terminalCancel: 'terminal:cancel',
/** main → renderer push channel for live terminal output lines */
terminalOutput: 'terminal:output'
} as const
/** Sentinel format id meaning "let yt-dlp pick the best video+audio". */
export const BEST_FORMAT_ID = '__best__'
export type MediaKind = 'video' | 'audio'
// --- Post-processing / format options ---------------------------------------
/** Audio extraction target. 'best' keeps the source codec without re-encoding. */
export type AudioFormat = 'best' | 'mp3' | 'm4a' | 'opus' | 'flac' | 'wav' | 'aac'
export const AUDIO_FORMATS: AudioFormat[] = ['best', 'mp3', 'm4a', 'opus', 'flac', 'wav', 'aac']
/** Container the merged video is muxed into (`--merge-output-format`). */
export type VideoContainer = 'mp4' | 'mkv' | 'webm'
export const VIDEO_CONTAINERS: VideoContainer[] = ['mp4', 'mkv', 'webm']
/** Preferred video codec, applied as a `-S vcodec:…` sort tiebreaker. */
export type VideoCodecPref = 'any' | 'h264' | 'vp9' | 'av1'
export const VIDEO_CODECS: VideoCodecPref[] = ['any', 'h264', 'vp9', 'av1']
/** Whether SponsorBlock cuts the segments out or just marks them as chapters. */
export type SponsorBlockMode = 'remove' | 'mark'
/**
* Where yt-dlp's auth cookies come from. 'browser' reads a logged-in browser's
* cookie store directly; 'login' uses a cookie file exported from AeroFetch's
* own built-in sign-in window (see src/main/cookies.ts).
*/
export type CookieSource = 'none' | 'browser' | 'login'
/** Browsers yt-dlp's --cookies-from-browser supports that ship on Windows. */
export const COOKIE_BROWSERS = ['chrome', 'edge', 'firefox', 'brave', 'opera', 'vivaldi'] as const
export type CookieBrowser = (typeof COOKIE_BROWSERS)[number]
/** Accent color presets for the brand ramp (see src/renderer/src/theme.ts). */
export const ACCENT_COLORS = ['rose', 'coral', 'amber', 'teal'] as const
export type AccentColor = (typeof ACCENT_COLORS)[number]
/** UI color theme: an explicit mode, or 'system' to follow the OS preference. */
export type ThemeMode = 'light' | 'dark' | 'system'
/** OS-level theme signal, read from Electron's nativeTheme in the main process. */
export interface SystemThemeInfo {
/** whether Windows is currently in dark mode */
shouldUseDarkColors: boolean
/** whether a Windows high-contrast theme is active */
shouldUseHighContrastColors: boolean
}
/** SponsorBlock segment categories (subset of yt-dlp's, the ones Seal exposes). */
export const SPONSORBLOCK_CATEGORIES = [
'sponsor',
'intro',
'outro',
'selfpromo',
'preview',
'filler',
'interaction',
'music_offtopic'
] as const
export type SponsorBlockCategory = (typeof SPONSORBLOCK_CATEGORIES)[number]
/**
* Post-processing / format choices applied to a download. The persisted
* defaults live in `Settings.downloadOptions`; an individual download may
* override them via `StartDownloadOptions.options`.
*/
export interface DownloadOptions {
/** audio extraction target (audio downloads only) */
audioFormat: AudioFormat
/** container for merged video downloads */
videoContainer: VideoContainer
/** preferred video codec (sort tiebreaker, not a hard filter) */
preferredVideoCodec: VideoCodecPref
/**
* Advanced: a raw yt-dlp `-S` format-sort string (e.g. 'res:1080,vcodec:av01,size').
* When non-empty it replaces the codec-derived sort, letting power users rank
* resolution / codec / container / size by weighted priority. Empty = use the
* preferredVideoCodec tiebreaker.
*/
formatSort: string
/** download + embed subtitles into video */
embedSubtitles: boolean
/** comma-separated yt-dlp sub-lang selectors, e.g. 'en' or 'en.*,es' */
subtitleLanguages: string
/** also pull auto-generated (ASR) captions */
autoSubtitles: boolean
/** enable SponsorBlock */
sponsorBlock: boolean
sponsorBlockMode: SponsorBlockMode
sponsorBlockCategories: SponsorBlockCategory[]
/** embed chapter markers */
embedChapters: boolean
/** split the download into one file per chapter (--split-chapters) */
splitChapters: boolean
/** embed metadata (title/artist/etc.) */
embedMetadata: boolean
/** embed the thumbnail (cover art for audio, poster for mp4/mkv video) */
embedThumbnail: boolean
/** crop embedded audio artwork to a centred square (Seal's "crop artwork") */
cropThumbnail: boolean
/** write a .info.json metadata sidecar (Jellyfin/Plex/Kodi ingest — Phase K) */
writeInfoJson: boolean
/** write the thumbnail as a separate image file alongside the video */
writeThumbnailFile: boolean
/** write the video description to a .description sidecar file */
writeDescription: boolean
}
export const DEFAULT_DOWNLOAD_OPTIONS: DownloadOptions = {
audioFormat: 'mp3',
videoContainer: 'mp4',
preferredVideoCodec: 'any',
formatSort: '',
embedSubtitles: false,
subtitleLanguages: 'en',
autoSubtitles: false,
sponsorBlock: false,
sponsorBlockMode: 'remove',
sponsorBlockCategories: ['sponsor'],
embedChapters: false,
splitChapters: false,
embedMetadata: true,
embedThumbnail: true,
cropThumbnail: false,
writeInfoJson: false,
writeThumbnailFile: false,
writeDescription: false
}
export interface YtdlpVersionResult {
ok: boolean
/** yt-dlp version string, present when ok is true */
version?: string
/** human-readable error, present when ok is false */
error?: string
}
/**
* Versions of the bundled ffmpeg + ffprobe, for display in Settings. Each is the
* parsed version token, or null when that binary is missing or unreadable. These
* ship with AeroFetch and aren't auto-updated (ffmpeg has no self-update and,
* unlike yt-dlp, doesn't break as sites change), so there's no ok/error here —
* just "what's installed".
*/
export interface FfmpegVersionResult {
ffmpeg: string | null
ffprobe: string | null
}
/** Result of checking the configured Gitea repo for a newer AeroFetch release. */
export interface AppUpdateInfo {
ok: boolean
/** true when latestVersion is newer than the running app */
available: boolean
/** the running app's version, e.g. '0.4.0' */
currentVersion: string
/** the latest release's version (tag without a leading 'v'), when ok */
latestVersion?: string
/** the release notes / changelog body (Markdown), shown before updating */
notes?: string
/** the release's web page on Gitea (for "View release") */
htmlUrl?: string
/** direct download URL of the Windows installer asset, when the release has one */
downloadUrl?: string
/** the installer asset's file name */
assetName?: string
/** human-readable error, when ok is false */
error?: string
}
/** Result of downloading the update installer to a temp file. */
export interface AppUpdateDownload {
ok: boolean
/** absolute path to the downloaded installer, when ok */
filePath?: string
error?: string
}
/** Live progress of the installer download (main → renderer). */
export interface AppUpdateProgress {
/** bytes downloaded so far */
received: number
/** total bytes, when the server reports Content-Length */
total?: number
/** 0..1 fraction, when total is known */
fraction?: number
}
/**
* yt-dlp's self-update release channels (`--update-to <channel>`).
*
* SECURITY (audit F1): `--update-to` also accepts an `OWNER/REPO@TAG` spec, which
* makes yt-dlp download a release binary from an ARBITRARY GitHub repo and
* overwrite the running yt-dlp.exe — i.e. arbitrary, persistent code execution.
* The TypeScript type is erased at runtime and is no defence over IPC, so the
* value must be checked against this allowlist before it ever reaches the flag
* (see isYtdlpUpdateChannel; enforced in src/main/ytdlp.ts).
*/
export const YTDLP_UPDATE_CHANNELS = ['stable', 'nightly'] as const
export type YtdlpUpdateChannel = (typeof YTDLP_UPDATE_CHANNELS)[number]
/** Runtime guard for an untrusted update-channel value crossing the IPC boundary. */
export function isYtdlpUpdateChannel(v: unknown): v is YtdlpUpdateChannel {
return typeof v === 'string' && (YTDLP_UPDATE_CHANNELS as readonly string[]).includes(v)
}
export interface YtdlpUpdateResult {
ok: boolean
/** yt-dlp's own update output, e.g. "Updated to ... " or "yt-dlp is up to date" */
output?: string
error?: string
}
/**
* Status of a background (startup) yt-dlp auto-update, pushed main → renderer on
* IpcChannels.ytdlpAutoUpdateStatus so the Settings UI can reflect a check that
* ran on its own — no button click needed.
*/
export interface YtdlpAutoUpdateStatus {
/** 'checking' when the update starts; the rest are terminal */
phase: 'checking' | 'updated' | 'current' | 'error'
/** which channel was checked */
channel: YtdlpUpdateChannel
/** yt-dlp version after the run, when known (phases 'updated' | 'current') */
version?: string
/** epoch ms the check finished (terminal phases) — feeds the "last checked" line */
checkedAt?: number
/** error text when phase === 'error' */
error?: string
}
/** A single selectable output format, derived from `yt-dlp -J`. */
export interface FormatOption {
/** yt-dlp format_id, or BEST_FORMAT_ID for the auto-best option */
id: string
/** human label, e.g. '1080p60 · mp4 · 124 MB' */
label: string
height?: number
ext?: string
filesizeLabel?: string
/** true when this format already carries an audio track */
hasAudio: boolean
}
/** Metadata + available formats returned by a probe. */
export interface MediaInfo {
title: string
channel?: string
durationLabel?: string
thumbnail?: string
/** video format options, best-first (audio is transcoded, so not listed) */
formats: FormatOption[]
}
/** One entry of a probed playlist (flat — no per-entry format list). */
export interface PlaylistEntry {
/** 1-based position in the playlist */
index: number
id: string
title: string
/** canonical URL for the entry, enqueued as its own single-video download */
url: string
durationLabel?: string
uploader?: string
}
/** A probed playlist: lightweight metadata for each entry. */
export interface PlaylistInfo {
title: string
uploader?: string
count: number
entries: PlaylistEntry[]
}
/**
* A single probe can resolve to either one video (with formats) or a playlist
* (with entries). `kind` discriminates which field is populated.
*/
export interface ProbeResult {
ok: boolean
kind?: 'video' | 'playlist'
info?: MediaInfo
playlist?: PlaylistInfo
error?: string
}
/**
* Media-manager folder context for a collection download (see ROADMAP-PINCHFLAT.md
* Phase G). When present on a StartDownloadOptions, the file is filed into
* `<outputDir>/<channel>/<playlist>/<NNN> - <title>.<ext>` instead of the flat
* filenameTemplate. The directory segments are sanitized for Windows at argv-build
* time, and `index` (the 1-based playlist position from the persisted MediaItem)
* drives the NNN prefix — not yt-dlp's %(playlist_index)s, which is empty under
* the per-video `--no-playlist` path these downloads still take.
*/
export interface CollectionContext {
/** channel / uploader name — the top folder level */
channel: string
/** playlist title — the second folder level ('Uploads' for the catch-all) */
playlist: string
/** 1-based position within the playlist, for the NNN filename prefix */
index: number
}
/** Sent renderer → main to kick off a download. The renderer owns the id. */
export interface StartDownloadOptions {
id: string
url: string
kind: MediaKind
/** one of the UI quality labels (e.g. '1080p', 'Best (MP3)') */
quality: string
/** absolute output directory; main falls back to the OS Downloads folder */
outputDir?: string
/** exact yt-dlp format_id chosen from a probe; omit for the preset path */
formatId?: string
/** whether the chosen format already includes audio (skips +bestaudio) */
formatHasAudio?: boolean
/** per-download post-processing override; main falls back to settings defaults */
options?: DownloadOptions
/**
* Per-download custom-command override: raw extra yt-dlp flags appended to
* the generated argv (see CommandTemplate). Omit to fall back to the
* persisted settings default (customCommandEnabled + defaultTemplateId);
* pass an empty string to explicitly run with no extra args for just this
* download even when the settings default is enabled.
*/
extraArgs?: string
/**
* Optional trim: keep only these time ranges. Raw user text — ranges like
* '1:30-2:00', comma- or newline-separated — normalised to yt-dlp
* `--download-sections` specs in buildArgs (parseTrimSections). Empty or
* undefined downloads the whole media.
*/
trim?: string
/**
* Metadata the renderer already fetched when probing the URL (title/channel/
* duration). When present, main uses it directly instead of spawning a second
* yt-dlp `--skip-download` probe — avoiding doubled network traffic and the
* race where the late probe overwrites a good title the renderer supplied.
*/
meta?: DownloadMeta
/**
* When set, this is a media-manager (collection) download — file it into
* `<channel>/<playlist>/<NNN> - <title>` folders instead of the flat
* filenameTemplate. See CollectionContext.
*/
collection?: CollectionContext
}
export interface StartDownloadResult {
ok: boolean
error?: string
}
export interface DownloadMeta {
title?: string
channel?: string
durationLabel?: string
}
export interface DownloadProgress {
/** yt-dlp progress status: 'downloading' | 'finished' | … */
status: string
/** 0..1 (0 when total size is unknown) */
progress: number
/** human-readable, e.g. '4.7 MB/s' */
speed?: string
/** human-readable, e.g. '1:12' */
eta?: string
/** human-readable total size, e.g. '512 MB' */
sizeLabel?: string
}
/** Discriminated union pushed on IpcChannels.downloadEvent. */
export type DownloadEvent =
| { type: 'meta'; id: string; meta: DownloadMeta }
| { type: 'progress'; id: string; progress: DownloadProgress }
| { type: 'done'; id: string; filePath?: string }
| { type: 'error'; id: string; error: string }
/** Persisted user settings (electron-store). */
export interface Settings {
/** where video downloads are saved; empty string = the default Documents\Video folder */
videoDir: string
/** where audio downloads are saved; empty string = the default Documents\Audio folder */
audioDir: string
defaultKind: MediaKind
defaultVideoQuality: string
defaultAudioQuality: string
/** how many downloads run at once */
maxConcurrent: number
/** yt-dlp output template, e.g. '%(title)s.%(ext)s' */
filenameTemplate: string
/** UI color theme: explicit light/dark, or 'system' to follow the OS */
theme: ThemeMode
/** brand accent preset (buttons, links, selected nav item) */
accentColor: AccentColor
/** auto-detect video links from the clipboard on window focus */
clipboardWatch: boolean
/** default post-processing / format options for new downloads */
downloadOptions: DownloadOptions
/** yt-dlp --proxy value, e.g. 'socks5://127.0.0.1:1080'. Empty disables it. */
proxy: string
/** yt-dlp --limit-rate value, e.g. '2M' or '500K'. Empty means unlimited. */
rateLimit: string
/** use bundled aria2c (resources/bin/aria2c.exe) as the external downloader */
useAria2c: boolean
/** where auth cookies come from: none, a browser's store, or the sign-in window */
cookieSource: CookieSource
/** which browser to read cookies from when cookieSource is 'browser' */
cookiesBrowser: CookieBrowser
/** sanitize output filenames to a portable ASCII-only subset (--restrict-filenames) */
restrictFilenames: boolean
/** record completed downloads in a yt-dlp --download-archive file to skip repeats */
downloadArchive: boolean
/** keep the managed yt-dlp.exe auto-updated in the background on launch */
autoUpdateYtdlp: boolean
/** channel the auto-updater (and the manual "Update yt-dlp" button) updates to */
ytdlpChannel: YtdlpUpdateChannel
/** epoch ms of the last automatic yt-dlp update check; 0 = never run */
ytdlpLastUpdateCheck: number
/** apply a named custom-command template's extra args to every new download */
customCommandEnabled: boolean
/** id of the CommandTemplate applied when customCommandEnabled is on; null = none picked yet */
defaultTemplateId: string | null
/** show a native OS notification when a download finishes or fails */
notifyOnComplete: boolean
/** auto-enqueue newly-found videos from watched sources during a sync (Phase J) */
autoDownloadNew: boolean
/** false until the first-run welcome screen has been dismissed */
hasCompletedOnboarding: boolean
}
/**
* A named, reusable set of extra yt-dlp CLI flags (Phase C "custom commands"),
* e.g. name: 'Write thumbnail + no mtime', args: '--write-thumbnail --no-mtime'.
* Shell-split (see parseExtraArgs) and appended to the generated argv just
* before the `--` URL terminator, so they can override earlier flags.
*/
export interface CommandTemplate {
id: string
name: string
args: string
/**
* Optional regex (matched case-insensitively against the download URL). When
* set and custom commands are enabled, this template auto-applies to any URL it
* matches — taking precedence over the global defaultTemplateId. Empty/undefined
* means the template is only applied when explicitly chosen as the default.
*/
urlPattern?: string
}
/** Result of building the exact yt-dlp command line for the current form state, without running it. */
export interface CommandPreviewResult {
ok: boolean
/** full command line (binary + quoted args), for display/copy only */
command?: string
error?: string
}
/** Whether the built-in sign-in window has ever saved a cookie file, and when. */
export interface CookiesStatus {
exists: boolean
/** epoch milliseconds the cookie file was last written */
savedAt?: number
}
/** Result of a built-in sign-in window session, resolved once the window closes. */
export interface CookiesLoginResult {
ok: boolean
/** number of cookies captured into the exported file */
cookieCount?: number
error?: string
}
/** A completed download, persisted to history.json (newest-first). */
export interface HistoryEntry {
id: string
title: string
channel?: string
url: string
kind: MediaKind
quality: string
filePath?: string
sizeLabel?: string
thumbnail?: string
/** epoch milliseconds */
completedAt: number
}
/**
* A failed download or download-start attempt, persisted to errorlog.json
* (newest-first) so the report survives the queue item being cleared —
* Seal's "debug report" equivalent.
*/
export interface ErrorLogEntry {
id: string
/** resolved video title when known; falls back to the URL in the UI */
title?: string
url: string
kind: MediaKind
error: string
/** epoch milliseconds */
occurredAt: number
}
/** Result of exporting settings + custom-command templates to a JSON file. */
export interface BackupExportResult {
ok: boolean
/** absolute path written to, present when ok is true */
path?: string
error?: string
}
/** Result of restoring settings + custom-command templates from a JSON file. */
export interface BackupImportResult {
ok: boolean
error?: string
}
// --- Sources & media-manager indexing (Pinchflat-style) ---------------------
// See ROADMAP-PINCHFLAT.md. A Source (channel/playlist) is indexed once into a
// persisted list of MediaItem records; the live download queue then pulls from
// that list a batch at a time, so "download an entire channel" never has to hold
// the whole collection in the queue at once.
/** Whether a Source is a whole channel or a single playlist. */
export type SourceKind = 'channel' | 'playlist'
/**
* A monitored channel or playlist that AeroFetch has indexed. Its full video
* list lives as MediaItem records (one JSON store), keyed back by `id`.
*/
export interface Source {
id: string
/** the URL the user added (used for re-indexing) */
url: string
kind: SourceKind
title: string
/** channel / uploader name, when known */
channel?: string
/** epoch ms the source was first added */
addedAt: number
/** epoch ms of the last successful (re)index; undefined if never indexed */
lastIndexedAt?: number
/** number of MediaItems at the last index (cached for list display) */
itemCount: number
/** watched for new uploads — included in scheduled / startup sync (Phase J) */
watched?: boolean
/** YouTube RSS feed URL for cheap "anything new?" checks; undefined if unknown */
feedUrl?: string
}
/**
* One video discovered while indexing a Source. `playlistTitle`/`playlistIndex`
* drive the on-disk folder layout (<Channel>/<Playlist>/<NNN> - <Title>); the
* `downloaded` flag + `filePath` let the library view show per-item state and
* let re-syncs skip what's already on disk.
*/
export interface MediaItem {
/** globally unique, formed as `${sourceId}:${videoId}` */
id: string
sourceId: string
/** the yt-dlp video id — the dedup key within a source */
videoId: string
title: string
url: string
/** the playlist this item is filed under; 'Uploads' for videos in no playlist */
playlistTitle: string
/** 1-based position within its playlist, for NNN numbering */
playlistIndex: number
durationLabel?: string
downloaded: boolean
downloadedAt?: number
filePath?: string
}
/** Live progress pushed while a Source is being indexed (main → renderer). */
export interface IndexProgress {
/** the Source URL being indexed (correlates events back to the request) */
url: string
phase: 'start' | 'playlists' | 'playlist' | 'uploads' | 'done' | 'error'
/** human-readable status line */
message: string
/** for the per-playlist phase: playlists processed so far / total */
current?: number
total?: number
}
/** Result of indexing (or re-indexing) a Source. */
export interface IndexSourceResult {
ok: boolean
source?: Source
itemCount?: number
/** how many videos were new since the previous index (all of them on first index) */
newCount?: number
error?: string
}
/** Result of syncing all watched sources: the videos found new across them. */
export interface SyncResult {
ok: boolean
/** newly-discovered media items across all watched sources (empty if nothing new) */
newItems: MediaItem[]
error?: string
}
/** Whether the Windows scheduled-sync task is currently registered. */
export interface ScheduledSyncStatus {
enabled: boolean
error?: string
}
// --- Built-in yt-dlp terminal (Phase N) -------------------------------------
// A power-user console that runs the bundled yt-dlp with raw, user-typed args
// and streams its output. The binary is fixed to yt-dlp (never arbitrary exes),
// and the feature is gated on the same customCommandEnabled consent flag as the
// per-download extra args (extra args can run code via --exec — audit F2).
/** Live output pushed on IpcChannels.terminalOutput while a terminal command runs. */
export type TerminalEvent =
| { type: 'output'; id: string; line: string; stream: 'stdout' | 'stderr' }
| { type: 'done'; id: string; code: number | null }
| { type: 'error'; id: string; error: string }
/** Result of starting a terminal command (pre-spawn validation only). */
export interface TerminalRunResult {
ok: boolean
error?: string
}