H1: split store/downloads.ts into types + item builder + preview seed
Extract from the 614-line store:
- downloadTypes.ts: all public types (DownloadItem, AddOptions, AddEntry,
ChosenFormat, DownloadStatus, DownloadState). Re-exported from downloads.ts
so every `import { ... } from '../store/downloads'` site is unchanged.
- downloadItem.ts: buildItem/titleFromUrl + the RESOLVING placeholder.
- downloadSeed.ts: the preview-only mock seed array.
downloads.ts now holds only the store, recordCompletion, and the preview
ticker. Behaviour unchanged; 248 tests + typecheck green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import { type DownloadMeta, type MediaKind } from '@shared/ipc'
|
||||||
|
import { newId } from '../id'
|
||||||
|
import { youtubeId } from '../lib/urlHelpers'
|
||||||
|
import { type AddOptions, type DownloadItem } from './downloadTypes'
|
||||||
|
|
||||||
|
// Placeholder channel shown while metadata is still being fetched. Tracked as a
|
||||||
|
// constant so the meta/error/done handlers can recognise and clear it instead of
|
||||||
|
// letting it stick forever when a probe returns no channel (SR6).
|
||||||
|
export const RESOLVING = 'Resolving…'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a fresh queued DownloadItem from a URL + options. Pure (no store access)
|
||||||
|
* so addFromUrl and addMany share it. If the caller already probed the URL, its
|
||||||
|
* metadata is carried as probedMeta so main can skip a redundant probe (audit P2).
|
||||||
|
*/
|
||||||
|
export function buildItem(
|
||||||
|
url: string,
|
||||||
|
kind: MediaKind,
|
||||||
|
quality: string,
|
||||||
|
opts?: AddOptions
|
||||||
|
): DownloadItem {
|
||||||
|
const probedMeta: DownloadMeta | undefined =
|
||||||
|
opts?.title || opts?.channel || opts?.durationLabel
|
||||||
|
? { title: opts?.title, channel: opts?.channel, durationLabel: opts?.durationLabel }
|
||||||
|
: undefined
|
||||||
|
// A future scheduled time parks the item as 'saved' until its moment arrives;
|
||||||
|
// a past/absent time starts it queued as usual.
|
||||||
|
const scheduledFor =
|
||||||
|
opts?.scheduledFor && opts.scheduledFor > Date.now() ? opts.scheduledFor : undefined
|
||||||
|
return {
|
||||||
|
id: newId('item'),
|
||||||
|
url,
|
||||||
|
title: opts?.title ?? titleFromUrl(url),
|
||||||
|
channel: opts?.channel ?? RESOLVING,
|
||||||
|
durationLabel: opts?.durationLabel,
|
||||||
|
thumbnail: opts?.thumbnail,
|
||||||
|
kind,
|
||||||
|
quality,
|
||||||
|
status: scheduledFor ? 'saved' : 'queued',
|
||||||
|
progress: 0,
|
||||||
|
formatId: opts?.format?.id,
|
||||||
|
formatHasAudio: opts?.format?.hasAudio,
|
||||||
|
options: opts?.options,
|
||||||
|
extraArgs: opts?.extraArgs,
|
||||||
|
trim: opts?.trim,
|
||||||
|
scheduledFor,
|
||||||
|
incognito: opts?.incognito,
|
||||||
|
collection: opts?.collection,
|
||||||
|
mediaItemId: opts?.mediaItemId,
|
||||||
|
probedMeta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function titleFromUrl(url: string): string {
|
||||||
|
const id = youtubeId(url)
|
||||||
|
if (id) return `YouTube video (${id})`
|
||||||
|
try {
|
||||||
|
const host = new URL(url).hostname
|
||||||
|
if (host) return `${host} download`
|
||||||
|
} catch {
|
||||||
|
/* not a URL */
|
||||||
|
}
|
||||||
|
return 'New download'
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { isPreview as PREVIEW } from '../isPreview'
|
||||||
|
import { newId } from '../id'
|
||||||
|
import { type DownloadItem } from './downloadTypes'
|
||||||
|
|
||||||
|
// Mock seed data so every visual state is visible in the UI preview. Empty in a
|
||||||
|
// real run -- the queue starts blank.
|
||||||
|
export const seed: DownloadItem[] = PREVIEW
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: newId('item'),
|
||||||
|
url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
title: 'Building a Desktop App with Electron -- Full Course',
|
||||||
|
channel: 'DevChannel',
|
||||||
|
durationLabel: '1:42:08',
|
||||||
|
kind: 'video',
|
||||||
|
quality: '1080p',
|
||||||
|
status: 'downloading',
|
||||||
|
progress: 0.42,
|
||||||
|
speedBytesPerSec: 4.7 * 1024 * 1024,
|
||||||
|
etaSeconds: 72,
|
||||||
|
sizeLabel: '512 MB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: newId('item'),
|
||||||
|
url: 'https://youtube.com/watch?v=abcd1234',
|
||||||
|
title: 'Lo-fi beats to code to (1 hour mix)',
|
||||||
|
channel: 'ChillStudio',
|
||||||
|
durationLabel: '1:00:21',
|
||||||
|
kind: 'audio',
|
||||||
|
quality: 'Best',
|
||||||
|
status: 'queued',
|
||||||
|
progress: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: newId('item'),
|
||||||
|
url: 'https://youtube.com/watch?v=zzz9999',
|
||||||
|
title: 'How yt-dlp Works Under the Hood',
|
||||||
|
channel: 'Open Source Weekly',
|
||||||
|
durationLabel: '14:03',
|
||||||
|
kind: 'video',
|
||||||
|
quality: '720p',
|
||||||
|
status: 'completed',
|
||||||
|
progress: 1,
|
||||||
|
sizeLabel: '184 MB',
|
||||||
|
filePath: 'C:\\Users\\you\\Downloads\\How yt-dlp Works Under the Hood.mp4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: newId('item'),
|
||||||
|
url: 'https://youtube.com/watch?v=err0r',
|
||||||
|
title: 'Private video',
|
||||||
|
channel: undefined,
|
||||||
|
kind: 'video',
|
||||||
|
quality: 'Best available',
|
||||||
|
status: 'error',
|
||||||
|
progress: 0,
|
||||||
|
error: 'Video unavailable: this video is private.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
type DownloadEvent,
|
||||||
|
type DownloadMeta,
|
||||||
|
type DownloadOptions,
|
||||||
|
type CollectionContext,
|
||||||
|
type MediaKind
|
||||||
|
} from '@shared/ipc'
|
||||||
|
|
||||||
|
// Single-sourced from the IPC contract (L105) and re-exported so existing
|
||||||
|
// `import { MediaKind } from '../store/downloads'` sites keep working.
|
||||||
|
export type { MediaKind }
|
||||||
|
|
||||||
|
export type DownloadStatus =
|
||||||
|
'queued' | 'downloading' | 'paused' | 'saved' | 'completed' | 'error' | 'canceled'
|
||||||
|
|
||||||
|
/** An exact probed format chosen for a download (omitted for the preset path). */
|
||||||
|
export interface ChosenFormat {
|
||||||
|
id: string
|
||||||
|
hasAudio: boolean
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadItem {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
channel?: string
|
||||||
|
durationLabel?: string
|
||||||
|
thumbnail?: string
|
||||||
|
kind: MediaKind
|
||||||
|
quality: string
|
||||||
|
status: DownloadStatus
|
||||||
|
/** 0..1 */
|
||||||
|
progress: number
|
||||||
|
/** raw transfer rate in bytes/sec (formatted for display via @shared/format) */
|
||||||
|
speedBytesPerSec?: number
|
||||||
|
/** raw time remaining in seconds (formatted for display via @shared/format) */
|
||||||
|
etaSeconds?: number
|
||||||
|
sizeLabel?: string
|
||||||
|
/** yt-dlp reported no total size -- render the bar indeterminate, not 0% (L137) */
|
||||||
|
sizeUnknown?: boolean
|
||||||
|
/** true once the main download stream finished and yt-dlp is fetching the
|
||||||
|
* remaining stream / merging / post-processing -- the bar goes indeterminate
|
||||||
|
* instead of visibly restarting 0→100% for the second stream (SR7) */
|
||||||
|
finishing?: boolean
|
||||||
|
filePath?: string
|
||||||
|
error?: string
|
||||||
|
/** exact format carried so retry re-downloads the same thing */
|
||||||
|
formatId?: string
|
||||||
|
formatHasAudio?: boolean
|
||||||
|
/** per-download post-processing override (omitted = use persisted defaults) */
|
||||||
|
options?: DownloadOptions
|
||||||
|
/** per-download custom-command override (omitted = use the persisted default template) */
|
||||||
|
extraArgs?: string
|
||||||
|
/** raw trim spec -- time ranges to keep (parsed to --download-sections in main) */
|
||||||
|
trim?: string
|
||||||
|
/** epoch ms to auto-start a 'saved' item; undefined = parked indefinitely (Phase M) */
|
||||||
|
scheduledFor?: number
|
||||||
|
/** private download -- completion is never recorded to history */
|
||||||
|
incognito?: boolean
|
||||||
|
/** metadata already fetched at probe time; forwarded to main so it skips a redundant probe */
|
||||||
|
probedMeta?: DownloadMeta
|
||||||
|
/** media-manager folder context -- files this into <channel>/<playlist>/<NNN> - <title> */
|
||||||
|
collection?: CollectionContext
|
||||||
|
/** when set, the source MediaItem id this download came from -- marked downloaded on completion */
|
||||||
|
mediaItemId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options accepted when enqueuing a URL (shared by addFromUrl + addMany). */
|
||||||
|
export interface AddOptions {
|
||||||
|
format?: ChosenFormat
|
||||||
|
title?: string
|
||||||
|
channel?: string
|
||||||
|
durationLabel?: string
|
||||||
|
thumbnail?: string
|
||||||
|
options?: DownloadOptions
|
||||||
|
extraArgs?: string
|
||||||
|
trim?: string
|
||||||
|
/** when set and in the future, the item starts 'saved' and auto-queues at this time */
|
||||||
|
scheduledFor?: number
|
||||||
|
incognito?: boolean
|
||||||
|
collection?: CollectionContext
|
||||||
|
mediaItemId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One URL to enqueue in a single addMany batch. */
|
||||||
|
export interface AddEntry {
|
||||||
|
url: string
|
||||||
|
kind: MediaKind
|
||||||
|
quality: string
|
||||||
|
opts?: AddOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadState {
|
||||||
|
items: DownloadItem[]
|
||||||
|
addFromUrl: (url: string, kind: MediaKind, quality: string, opts?: AddOptions) => void
|
||||||
|
/**
|
||||||
|
* Enqueue many URLs at once with a single state update and a single pump, so
|
||||||
|
* queuing an entire channel (1000s of items) doesn't churn the store O(n²) the
|
||||||
|
* way looping addFromUrl would.
|
||||||
|
*/
|
||||||
|
addMany: (entries: AddEntry[]) => void
|
||||||
|
retry: (id: string) => void
|
||||||
|
/** re-queue every failed item at once (Phase M) */
|
||||||
|
retryAll: () => void
|
||||||
|
/** stop a running download but keep its partial file for a later resume (Phase M) */
|
||||||
|
pause: (id: string) => void
|
||||||
|
/** re-queue a paused download; yt-dlp continues its partial file on relaunch */
|
||||||
|
resume: (id: string) => void
|
||||||
|
/** move a queued item to the front of the promotion order ("download next", Phase M) */
|
||||||
|
prioritize: (id: string) => void
|
||||||
|
/** park a queued item indefinitely as 'saved' (Phase M) */
|
||||||
|
saveForLater: (id: string) => void
|
||||||
|
/** un-park a 'saved' item back into the queue now (clears any schedule) */
|
||||||
|
queueNow: (id: string) => void
|
||||||
|
/** internal: promote any 'saved' item whose scheduledFor time has arrived */
|
||||||
|
promoteDueScheduled: () => void
|
||||||
|
cancel: (id: string) => void
|
||||||
|
remove: (id: string) => void
|
||||||
|
clearFinished: () => void
|
||||||
|
openFile: (id: string) => void
|
||||||
|
showInFolder: (id: string) => void
|
||||||
|
/** promote queued items into free download slots (respects MAX_CONCURRENT) */
|
||||||
|
pump: () => void
|
||||||
|
/** internal: apply a main-process download event */
|
||||||
|
applyEvent: (ev: DownloadEvent) => void
|
||||||
|
}
|
||||||
@@ -1,247 +1,23 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import {
|
import { type MediaKind } from '@shared/ipc'
|
||||||
type DownloadEvent,
|
|
||||||
type DownloadMeta,
|
|
||||||
type DownloadOptions,
|
|
||||||
type CollectionContext,
|
|
||||||
type MediaKind
|
|
||||||
} from '@shared/ipc'
|
|
||||||
import { isPreview as PREVIEW } from '../isPreview'
|
import { isPreview as PREVIEW } from '../isPreview'
|
||||||
import { useSettings } from './settings'
|
import { useSettings } from './settings'
|
||||||
import { useHistory } from './history'
|
import { useHistory } from './history'
|
||||||
import { emit, on } from './coordinator'
|
import { emit, on } from './coordinator'
|
||||||
import { newId } from '../id'
|
import { RESOLVING, buildItem } from './downloadItem'
|
||||||
import { youtubeId } from '../lib/urlHelpers'
|
import { seed } from './downloadSeed'
|
||||||
|
import {
|
||||||
|
type ChosenFormat,
|
||||||
|
type DownloadItem,
|
||||||
|
type DownloadStatus,
|
||||||
|
type AddOptions,
|
||||||
|
type AddEntry,
|
||||||
|
type DownloadState
|
||||||
|
} from './downloadTypes'
|
||||||
|
|
||||||
// Single-sourced from the IPC contract (L105) and re-exported so existing
|
// Re-export the store's public types from their canonical home so existing
|
||||||
// `import { MediaKind } from '../store/downloads'` sites keep working.
|
// `import { … } from '../store/downloads'` sites keep working.
|
||||||
export type { MediaKind }
|
export type { MediaKind, ChosenFormat, DownloadItem, DownloadStatus, AddOptions, AddEntry }
|
||||||
|
|
||||||
export type DownloadStatus =
|
|
||||||
'queued' | 'downloading' | 'paused' | 'saved' | 'completed' | 'error' | 'canceled'
|
|
||||||
|
|
||||||
/** An exact probed format chosen for a download (omitted for the preset path). */
|
|
||||||
export interface ChosenFormat {
|
|
||||||
id: string
|
|
||||||
hasAudio: boolean
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DownloadItem {
|
|
||||||
id: string
|
|
||||||
url: string
|
|
||||||
title: string
|
|
||||||
channel?: string
|
|
||||||
durationLabel?: string
|
|
||||||
thumbnail?: string
|
|
||||||
kind: MediaKind
|
|
||||||
quality: string
|
|
||||||
status: DownloadStatus
|
|
||||||
/** 0..1 */
|
|
||||||
progress: number
|
|
||||||
/** raw transfer rate in bytes/sec (formatted for display via @shared/format) */
|
|
||||||
speedBytesPerSec?: number
|
|
||||||
/** raw time remaining in seconds (formatted for display via @shared/format) */
|
|
||||||
etaSeconds?: number
|
|
||||||
sizeLabel?: string
|
|
||||||
/** yt-dlp reported no total size -- render the bar indeterminate, not 0% (L137) */
|
|
||||||
sizeUnknown?: boolean
|
|
||||||
/** true once the main download stream finished and yt-dlp is fetching the
|
|
||||||
* remaining stream / merging / post-processing -- the bar goes indeterminate
|
|
||||||
* instead of visibly restarting 0→100% for the second stream (SR7) */
|
|
||||||
finishing?: boolean
|
|
||||||
filePath?: string
|
|
||||||
error?: string
|
|
||||||
/** exact format carried so retry re-downloads the same thing */
|
|
||||||
formatId?: string
|
|
||||||
formatHasAudio?: boolean
|
|
||||||
/** per-download post-processing override (omitted = use persisted defaults) */
|
|
||||||
options?: DownloadOptions
|
|
||||||
/** per-download custom-command override (omitted = use the persisted default template) */
|
|
||||||
extraArgs?: string
|
|
||||||
/** raw trim spec -- time ranges to keep (parsed to --download-sections in main) */
|
|
||||||
trim?: string
|
|
||||||
/** epoch ms to auto-start a 'saved' item; undefined = parked indefinitely (Phase M) */
|
|
||||||
scheduledFor?: number
|
|
||||||
/** private download -- completion is never recorded to history */
|
|
||||||
incognito?: boolean
|
|
||||||
/** metadata already fetched at probe time; forwarded to main so it skips a redundant probe */
|
|
||||||
probedMeta?: DownloadMeta
|
|
||||||
/** media-manager folder context -- files this into <channel>/<playlist>/<NNN> - <title> */
|
|
||||||
collection?: CollectionContext
|
|
||||||
/** when set, the source MediaItem id this download came from -- marked downloaded on completion */
|
|
||||||
mediaItemId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder channel shown while metadata is still being fetched. Tracked as a
|
|
||||||
// constant so the meta/error/done handlers can recognise and clear it instead of
|
|
||||||
// letting it stick forever when a probe returns no channel (SR6).
|
|
||||||
const RESOLVING = 'Resolving…'
|
|
||||||
|
|
||||||
/** Options accepted when enqueuing a URL (shared by addFromUrl + addMany). */
|
|
||||||
export interface AddOptions {
|
|
||||||
format?: ChosenFormat
|
|
||||||
title?: string
|
|
||||||
channel?: string
|
|
||||||
durationLabel?: string
|
|
||||||
thumbnail?: string
|
|
||||||
options?: DownloadOptions
|
|
||||||
extraArgs?: string
|
|
||||||
trim?: string
|
|
||||||
/** when set and in the future, the item starts 'saved' and auto-queues at this time */
|
|
||||||
scheduledFor?: number
|
|
||||||
incognito?: boolean
|
|
||||||
collection?: CollectionContext
|
|
||||||
mediaItemId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One URL to enqueue in a single addMany batch. */
|
|
||||||
export interface AddEntry {
|
|
||||||
url: string
|
|
||||||
kind: MediaKind
|
|
||||||
quality: string
|
|
||||||
opts?: AddOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DownloadState {
|
|
||||||
items: DownloadItem[]
|
|
||||||
addFromUrl: (url: string, kind: MediaKind, quality: string, opts?: AddOptions) => void
|
|
||||||
/**
|
|
||||||
* Enqueue many URLs at once with a single state update and a single pump, so
|
|
||||||
* queuing an entire channel (1000s of items) doesn't churn the store O(n²) the
|
|
||||||
* way looping addFromUrl would.
|
|
||||||
*/
|
|
||||||
addMany: (entries: AddEntry[]) => void
|
|
||||||
retry: (id: string) => void
|
|
||||||
/** re-queue every failed item at once (Phase M) */
|
|
||||||
retryAll: () => void
|
|
||||||
/** stop a running download but keep its partial file for a later resume (Phase M) */
|
|
||||||
pause: (id: string) => void
|
|
||||||
/** re-queue a paused download; yt-dlp continues its partial file on relaunch */
|
|
||||||
resume: (id: string) => void
|
|
||||||
/** move a queued item to the front of the promotion order ("download next", Phase M) */
|
|
||||||
prioritize: (id: string) => void
|
|
||||||
/** park a queued item indefinitely as 'saved' (Phase M) */
|
|
||||||
saveForLater: (id: string) => void
|
|
||||||
/** un-park a 'saved' item back into the queue now (clears any schedule) */
|
|
||||||
queueNow: (id: string) => void
|
|
||||||
/** internal: promote any 'saved' item whose scheduledFor time has arrived */
|
|
||||||
promoteDueScheduled: () => void
|
|
||||||
cancel: (id: string) => void
|
|
||||||
remove: (id: string) => void
|
|
||||||
clearFinished: () => void
|
|
||||||
openFile: (id: string) => void
|
|
||||||
showInFolder: (id: string) => void
|
|
||||||
/** promote queued items into free download slots (respects MAX_CONCURRENT) */
|
|
||||||
pump: () => void
|
|
||||||
/** internal: apply a main-process download event */
|
|
||||||
applyEvent: (ev: DownloadEvent) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a fresh queued DownloadItem from a URL + options. Pure (no store access)
|
|
||||||
* so addFromUrl and addMany share it. If the caller already probed the URL, its
|
|
||||||
* metadata is carried as probedMeta so main can skip a redundant probe (audit P2).
|
|
||||||
*/
|
|
||||||
function buildItem(url: string, kind: MediaKind, quality: string, opts?: AddOptions): DownloadItem {
|
|
||||||
const probedMeta: DownloadMeta | undefined =
|
|
||||||
opts?.title || opts?.channel || opts?.durationLabel
|
|
||||||
? { title: opts?.title, channel: opts?.channel, durationLabel: opts?.durationLabel }
|
|
||||||
: undefined
|
|
||||||
// A future scheduled time parks the item as 'saved' until its moment arrives;
|
|
||||||
// a past/absent time starts it queued as usual.
|
|
||||||
const scheduledFor =
|
|
||||||
opts?.scheduledFor && opts.scheduledFor > Date.now() ? opts.scheduledFor : undefined
|
|
||||||
return {
|
|
||||||
id: newId('item'),
|
|
||||||
url,
|
|
||||||
title: opts?.title ?? titleFromUrl(url),
|
|
||||||
channel: opts?.channel ?? RESOLVING,
|
|
||||||
durationLabel: opts?.durationLabel,
|
|
||||||
thumbnail: opts?.thumbnail,
|
|
||||||
kind,
|
|
||||||
quality,
|
|
||||||
status: scheduledFor ? 'saved' : 'queued',
|
|
||||||
progress: 0,
|
|
||||||
formatId: opts?.format?.id,
|
|
||||||
formatHasAudio: opts?.format?.hasAudio,
|
|
||||||
options: opts?.options,
|
|
||||||
extraArgs: opts?.extraArgs,
|
|
||||||
trim: opts?.trim,
|
|
||||||
scheduledFor,
|
|
||||||
incognito: opts?.incognito,
|
|
||||||
collection: opts?.collection,
|
|
||||||
mediaItemId: opts?.mediaItemId,
|
|
||||||
probedMeta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleFromUrl(url: string): string {
|
|
||||||
const id = youtubeId(url)
|
|
||||||
if (id) return `YouTube video (${id})`
|
|
||||||
try {
|
|
||||||
const host = new URL(url).hostname
|
|
||||||
if (host) return `${host} download`
|
|
||||||
} catch {
|
|
||||||
/* not a URL */
|
|
||||||
}
|
|
||||||
return 'New download'
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Mock seed data so every visual state is visible in the UI preview ------
|
|
||||||
const seed: DownloadItem[] = PREVIEW
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: newId('item'),
|
|
||||||
url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
|
|
||||||
title: 'Building a Desktop App with Electron -- Full Course',
|
|
||||||
channel: 'DevChannel',
|
|
||||||
durationLabel: '1:42:08',
|
|
||||||
kind: 'video',
|
|
||||||
quality: '1080p',
|
|
||||||
status: 'downloading',
|
|
||||||
progress: 0.42,
|
|
||||||
speedBytesPerSec: 4.7 * 1024 * 1024,
|
|
||||||
etaSeconds: 72,
|
|
||||||
sizeLabel: '512 MB'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: newId('item'),
|
|
||||||
url: 'https://youtube.com/watch?v=abcd1234',
|
|
||||||
title: 'Lo-fi beats to code to (1 hour mix)',
|
|
||||||
channel: 'ChillStudio',
|
|
||||||
durationLabel: '1:00:21',
|
|
||||||
kind: 'audio',
|
|
||||||
quality: 'Best',
|
|
||||||
status: 'queued',
|
|
||||||
progress: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: newId('item'),
|
|
||||||
url: 'https://youtube.com/watch?v=zzz9999',
|
|
||||||
title: 'How yt-dlp Works Under the Hood',
|
|
||||||
channel: 'Open Source Weekly',
|
|
||||||
durationLabel: '14:03',
|
|
||||||
kind: 'video',
|
|
||||||
quality: '720p',
|
|
||||||
status: 'completed',
|
|
||||||
progress: 1,
|
|
||||||
sizeLabel: '184 MB',
|
|
||||||
filePath: 'C:\\Users\\you\\Downloads\\How yt-dlp Works Under the Hood.mp4'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: newId('item'),
|
|
||||||
url: 'https://youtube.com/watch?v=err0r',
|
|
||||||
title: 'Private video',
|
|
||||||
channel: undefined,
|
|
||||||
kind: 'video',
|
|
||||||
quality: 'Best available',
|
|
||||||
status: 'error',
|
|
||||||
progress: 0,
|
|
||||||
error: 'Video unavailable: this video is private.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
|
|
||||||
// Side-effects to run exactly once when a download completes: record it to
|
// Side-effects to run exactly once when a download completes: record it to
|
||||||
// history (unless private) and mark its source MediaItem downloaded. Shared by
|
// history (unless private) and mark its source MediaItem downloaded. Shared by
|
||||||
|
|||||||
Reference in New Issue
Block a user