/** * Shared contract between main and renderer. * IPC channel names + payload types live here so both sides stay in sync. */ export const IpcChannels = { ytdlpVersion: 'ytdlp:version', probe: 'media:probe', downloadStart: 'download:start', downloadCancel: 'download:cancel', 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', 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' } 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 = ['toffee', 'slate', 'evergreen', 'lavender'] 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 /** 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 /** 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 } export const DEFAULT_DOWNLOAD_OPTIONS: DownloadOptions = { audioFormat: 'mp3', videoContainer: 'mp4', preferredVideoCodec: 'any', embedSubtitles: false, subtitleLanguages: 'en', autoSubtitles: false, sponsorBlock: false, sponsorBlockMode: 'remove', sponsorBlockCategories: ['sponsor'], embedChapters: false, embedMetadata: true, embedThumbnail: true, cropThumbnail: 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 } /** yt-dlp's self-update release channel (`--update-to `). */ export type YtdlpUpdateChannel = 'stable' | 'nightly' 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 } /** 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 } /** 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 } 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 { /** absolute output directory (empty string resolves to the OS Downloads folder) */ outputDir: 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 /** 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 /** 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 } /** 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 }