feat(library): add per-row "Download new" for watched sources

Queuing a watched channel's not-yet-downloaded videos previously meant
expanding the source and using its "Download N pending" button. Watched
source rows now carry a one-click "Download new" that loads the source's
items (even while collapsed) and queues everything not already
downloaded or in the queue, using the source's resolved kind/profile.

Extracts the shared, tested selectUndownloaded selector (also now backs
the expanded panel's pending list) and adds ensureSourceItems to load a
collapsed source's items without expanding it.
This commit is contained in:
2026-07-05 17:49:39 -04:00
parent 27cae28e12
commit cb25262a2d
4 changed files with 176 additions and 33 deletions
+43
View File
@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest'
import { selectUndownloaded } from '../src/renderer/src/store/librarySelect'
import type { MediaItem } from '../src/shared/ipc'
const item = (over: Partial<MediaItem> & { videoId: string }): MediaItem => ({
id: `src:${over.videoId}`,
sourceId: 'src',
title: over.videoId,
url: `https://youtube.com/watch?v=${over.videoId}`,
playlistTitle: 'Uploads',
playlistIndex: 1,
downloaded: false,
...over
})
describe('selectUndownloaded', () => {
it('keeps items that are neither downloaded nor already queued', () => {
const items = [item({ videoId: 'a' }), item({ videoId: 'b' })]
const result = selectUndownloaded(items, new Set())
expect(result.map((i) => i.videoId)).toEqual(['a', 'b'])
})
it('excludes already-downloaded items', () => {
const items = [item({ videoId: 'a', downloaded: true }), item({ videoId: 'b' })]
expect(selectUndownloaded(items, new Set()).map((i) => i.videoId)).toEqual(['b'])
})
it('excludes items whose URL already has a queue entry (any status)', () => {
const queued = item({ videoId: 'a' })
const fresh = item({ videoId: 'b' })
const result = selectUndownloaded([queued, fresh], new Set([queued.url]))
expect(result.map((i) => i.videoId)).toEqual(['b'])
})
it('returns an empty array when nothing is queueable', () => {
const items = [item({ videoId: 'a', downloaded: true })]
expect(selectUndownloaded(items, new Set())).toEqual([])
})
it('returns an empty array for no items', () => {
expect(selectUndownloaded([], new Set())).toEqual([])
})
})