Files
AeroFetch/src/shared/ipc.ts
T
debont80 fa78b13cac Complete Phase E theming, onboarding, and Windows share integration
Adds theme presets (Toffee/Slate/Evergreen/Lavender) with a "follow system"
mode and high-contrast awareness, a first-run welcome screen, and the
Windows analog of Android's share sheet for a Win32 app: an aerofetch://
protocol handler plus an Explorer "Send to AeroFetch" entry, both routed
through a single-instance lock so a second launch hands its link to the
already-running window instead of opening a duplicate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 08:45:09 -04:00

406 lines
14 KiB
TypeScript

/**
* 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 <channel>`). */
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
}