feat: queue UX (Phase M) + media editing (split-chapters, trim)
Phase L (media editing): - Split by chapters (--split-chapters) as a DownloadOptions toggle - Trim/cut by time range: parseTrimSections -> --download-sections "*A-B" + --force-keyframes-at-cuts (deduped vs SponsorBlock), per-download Trim panel Phase M (queue & daily-use UX), all 7: - Pause/resume: main-side killTree + paused flag (silent close), download:pause IPC, store pause/resume + paused status, QueueItem controls - Reorder via "Download next" (prioritize) - Aggregate progress strip (pure summarizeQueue) + combined speed/ETA - Drag-and-drop links / .url files onto the download card - Retry all failed - Duplicate detection (sameVideo) with "Download anyway" guard - Per-download scheduling (datetime-local) + save-for-later (saved status, 15s promotion ticker); session-only (queue not persisted) New pure modules unit-tested (queueStats); buildArgs trim/split tested. typecheck + test green (178 passed). Roadmap updated: Phase M COMPLETE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { summarizeQueue, sameVideo } from '../src/renderer/src/store/queueStats'
|
||||
import type { DownloadItem } from '../src/renderer/src/store/downloads'
|
||||
|
||||
// Minimal item factory — only the fields summarizeQueue reads.
|
||||
function item(overrides: Partial<DownloadItem>): DownloadItem {
|
||||
return {
|
||||
id: Math.random().toString(36).slice(2),
|
||||
url: 'https://youtube.com/watch?v=x',
|
||||
title: 'x',
|
||||
kind: 'video',
|
||||
quality: '1080p',
|
||||
status: 'queued',
|
||||
progress: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('summarizeQueue', () => {
|
||||
it('counts statuses and flags active', () => {
|
||||
const s = summarizeQueue([
|
||||
item({ status: 'downloading', progress: 0.5 }),
|
||||
item({ status: 'queued' }),
|
||||
item({ status: 'error' }),
|
||||
item({ status: 'completed', progress: 1 }),
|
||||
item({ status: 'canceled' })
|
||||
])
|
||||
expect(s.downloading).toBe(1)
|
||||
expect(s.queued).toBe(1)
|
||||
expect(s.failed).toBe(1)
|
||||
expect(s.active).toBe(true)
|
||||
})
|
||||
|
||||
it('is inactive and zeroed when nothing is downloading or queued', () => {
|
||||
const s = summarizeQueue([item({ status: 'completed', progress: 1 })])
|
||||
expect(s.active).toBe(false)
|
||||
expect(s.progress).toBe(0)
|
||||
expect(s.speedLabel).toBe('')
|
||||
expect(s.etaLabel).toBe('')
|
||||
})
|
||||
|
||||
it('averages progress over downloading + queued (queued counts as 0%)', () => {
|
||||
// one at 80%, one queued at 0% → 40%
|
||||
const s = summarizeQueue([
|
||||
item({ status: 'downloading', progress: 0.8 }),
|
||||
item({ status: 'queued' })
|
||||
])
|
||||
expect(s.progress).toBeCloseTo(0.4, 5)
|
||||
})
|
||||
|
||||
it('sums download speeds across MB/s and MiB/s strings', () => {
|
||||
const s = summarizeQueue([
|
||||
item({ status: 'downloading', progress: 0.1, speed: '4.0 MB/s' }),
|
||||
item({ status: 'downloading', progress: 0.1, speed: '2000 KiB/s' })
|
||||
])
|
||||
// 4.0 MB/s + ~2.0 MB/s ≈ 6.0 MB/s
|
||||
expect(s.speedLabel).toBe('6.0 MB/s')
|
||||
})
|
||||
|
||||
it('reports the longest active ETA as the time left', () => {
|
||||
const s = summarizeQueue([
|
||||
item({ status: 'downloading', progress: 0.5, eta: '0:30' }),
|
||||
item({ status: 'downloading', progress: 0.2, eta: '2:10' })
|
||||
])
|
||||
expect(s.etaLabel).toBe('2:10')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sameVideo', () => {
|
||||
it('matches identical URLs', () => {
|
||||
expect(sameVideo('https://x.com/a', 'https://x.com/a')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches YouTube links by video id across watch/youtu.be/extra params', () => {
|
||||
expect(
|
||||
sameVideo(
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
'https://youtu.be/dQw4w9WgXcQ'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
sameVideo(
|
||||
'https://youtube.com/watch?v=dQw4w9WgXcQ&list=PL123',
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match different videos', () => {
|
||||
expect(
|
||||
sameVideo(
|
||||
'https://youtube.com/watch?v=aaaaaaaaaaa',
|
||||
'https://youtube.com/watch?v=bbbbbbbbbbb'
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('normalises www. and trailing slash for non-YouTube URLs', () => {
|
||||
expect(sameVideo('https://www.vimeo.com/123/', 'https://vimeo.com/123')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user