f167c02946
Closing the window no longer kills in-progress downloads, the library gains the same copied-link suggestion the downloads tab has, and the in-app updater can now authenticate to a sign-in-required Gitea. Background / tray: - The window's close handler now hides to the tray (instead of quitting) whenever a download is in flight, even if "Keep running in the tray" is off — quitting was killing the spawned yt-dlp processes. A one-time notification explains the app is still running. (download.ts exposes hasActiveDownloads().) - tray.ts now falls back to an embedded icon when no build/icon.ico ships, so the tray actually appears — previously an empty icon meant the tray was skipped and a minimized window could be stranded with no way back. - New "Start with Windows" setting (launchAtStartup) wired to app.setLoginItemSettings, synced at startup and on toggle. Useful with auto-download so watched channels stay current in the background. - The "Keep running in the tray" hint now explains it also enables background auto-download of new uploads. Library clipboard detection: - Extracted the downloads tab's clipboard watcher into a shared useClipboardLink hook and used it in the library's add-source field, so a copied channel/playlist link is offered there too. Updater fix (works on a private / sign-in-required instance): - Added an optional updateToken setting. When set, the updater sends it as a Gitea Authorization header on the release check, the checksum fetch, and the installer download — so "Check for updates" works where anonymous access is blocked (the previous "could not reach the update server" case). Blank = anonymous, unchanged. No token is ever shipped; it's only ever sent to the host-pinned update host. Settings gains a masked "Update access token" field. Typecheck clean; 187 tests pass; electron-vite build clean. New UI verified in the browser preview (tray/startup toggles, token field, library copied-link banner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
744 lines
28 KiB
TypeScript
744 lines
28 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',
|
|
/** renderer → main: reflect overall queue progress on the Windows taskbar (Phase O) */
|
|
taskbarProgress: 'taskbar:progress'
|
|
} 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
|
|
/**
|
|
* YouTube reliability (Phase P). When set, emitted as
|
|
* `--extractor-args "youtube:player_client=…;po_token=…"`:
|
|
* - playerClient: an alternate extraction client (e.g. 'web_safari', 'tv',
|
|
* 'mweb') — a common workaround for YouTube throttling/extraction breakage.
|
|
* - poToken: a manually-supplied Proof-of-Origin token for the bot check.
|
|
* Both empty = yt-dlp's defaults. (Automatic WebView token minting is deferred;
|
|
* this is the argv plumbing it would feed.)
|
|
*/
|
|
youtubePlayerClient: string
|
|
youtubePoToken: string
|
|
/** 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
|
|
/** keep AeroFetch running in the system tray when its window is closed (Phase O) */
|
|
minimizeToTray: boolean
|
|
/** launch AeroFetch automatically when the user signs in to Windows */
|
|
launchAtStartup: boolean
|
|
/**
|
|
* Optional Gitea access token for the in-app updater. Empty = anonymous (works
|
|
* only where the release repo allows anonymous access). When the release repo
|
|
* is private / the instance requires sign-in, paste a read-only token so the
|
|
* updater can check + download. Stored locally in settings, never shipped.
|
|
*/
|
|
updateToken: string
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/** Overall queue state for the Windows taskbar progress bar (Phase O). */
|
|
export interface TaskbarProgress {
|
|
/** 0..1 overall fraction; ignored when mode is 'none' */
|
|
fraction: number
|
|
/** taskbar bar mode: hidden, normal, or red (some failed) */
|
|
mode: 'none' | 'normal' | 'error'
|
|
}
|