diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index d8df109..e5ba987 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -289,7 +289,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n plain JSON file; the UI caption only says "Does not include download history" — no warning that the file contains credentials. *Fixed: `proxy`/`youtubePoToken`/`updateToken` are stripped (set to `''`) before writing; SettingsView caption updated to say credentials are not included.* -- [ ] **M23 — `MAX_ITEMS` truncation can silently drop a source's items.** [sources.ts](src/main/sources.ts) +- [x] **M23 — `MAX_ITEMS` truncation can silently drop a source's items.** [sources.ts](src/main/sources.ts) `replaceMediaItems` does `[...new, ...others].slice(0, 20000)`; once the global total exceeds the cap, the *tail* (another source's items) is dropped on write and only reappears when that source is re-indexed. Cap per-source, or surface it. @@ -309,7 +309,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n Toffee; the shipped [theme.ts](src/renderer/src/theme.ts) is rose / coral / amber / teal ("Sunset-to-sea") with a default of **teal**. The whole section documents a palette that no longer exists. -- [ ] **M27 — Library batch-enqueue ignores per-item kind.** [sources.ts](src/renderer/src/store/sources.ts) +- [x] **M27 — Library batch-enqueue ignores per-item kind.** [sources.ts](src/renderer/src/store/sources.ts) `enqueueItems` forces `settings.defaultKind` (+ its quality) for *every* item, so a channel can't be downloaded as audio without flipping the global default — inconsistent with the DownloadBar playlist panel, which offers a per-item video/audio toggle. diff --git a/src/main/sources.ts b/src/main/sources.ts index 34a579b..17e181c 100644 --- a/src/main/sources.ts +++ b/src/main/sources.ts @@ -2,11 +2,11 @@ * Persistence for the media-manager index (Pinchflat-style; see * ROADMAP-PINCHFLAT.md). Two plain-JSON stores in userData, mirroring the * history.ts pattern (and the same deliberate choice to stay on JSON rather than - * better-sqlite3 — revisit if a user indexes many large channels, see the + * better-sqlite3 -- revisit if a user indexes many large channels, see the * Phase H risk note in the roadmap): * - * sources.json — one Source record per added channel/playlist - * media-items.json — every MediaItem across all sources (queried by sourceId) + * sources.json -- one Source record per added channel/playlist + * media-items.json -- every MediaItem across all sources (queried by sourceId) * * Per-row validation on read (validation.ts) so a hand-edited or corrupted file * can't feed the UI malformed records (same approach as history/errorlog). @@ -68,12 +68,14 @@ export function listMediaItems(sourceId: string): MediaItem[] { /** * Replace all media items for one source with a fresh set (the result of a - * (re)index). Other sources' items are preserved; the new items are placed first - * so they survive the MAX_ITEMS cap. + * (re)index). Other sources' items are preserved in full; only the source being + * written here absorbs the MAX_ITEMS cap, so re-indexing one (large) source can + * never silently evict another source's items past the global limit (M23). */ export function replaceMediaItems(sourceId: string, items: MediaItem[]): void { const others = listAllItems().filter((m) => m.sourceId !== sourceId) - itemsStore.write([...items, ...others]) + const budget = Math.max(0, MAX_ITEMS - others.length) + itemsStore.write([...items.slice(0, budget), ...others]) } /** diff --git a/src/renderer/src/components/LibraryView.tsx b/src/renderer/src/components/LibraryView.tsx index d08046c..d96776c 100644 --- a/src/renderer/src/components/LibraryView.tsx +++ b/src/renderer/src/components/LibraryView.tsx @@ -27,7 +27,7 @@ import { LinkRegular, DismissRegular } from '@fluentui/react-icons' -import type { MediaItem, Source } from '@shared/ipc' +import type { MediaItem, Source, MediaKind } from '@shared/ipc' import { useSources } from '../store/sources' import { useSettings } from '../store/settings' import { useClipboardLink, looksLikeSingleVideo } from '../useClipboardLink' @@ -39,6 +39,7 @@ import { MediaThumb } from './MediaThumb' import { VirtualList } from './VirtualList' import { ScreenHeader, useScreenStyles } from './ui/Screen' import { StatusChip } from './ui/StatusChip' +import { SegmentedControl } from './ui/SegmentedControl' import { useFocusStyles } from './ui/focusRing' import { THUMB_XS } from '../thumbSizes' @@ -279,6 +280,11 @@ export function LibraryView(): React.JSX.Element { // videos -- a library source is a channel/playlist to sync, not a one-off. const clip = useClipboardLink(url, (u) => !looksLikeSingleVideo(u)) const [selected, setSelected] = useState>(new Set()) + // Per-batch video/audio choice for queueing items, seeded from the global + // default so behavior is unchanged until the user flips it (M27). + const [enqueueKind, setEnqueueKind] = useState( + () => useSettings.getState().defaultKind + ) const [batchNote, setBatchNote] = useState(null) const [syncNote, setSyncNote] = useState(null) // Which playlist groups are expanded. Empty = all collapsed (the default), so @@ -416,14 +422,14 @@ export function LibraryView(): React.JSX.Element { // run, the rest wait in the queue. Selection clears since nothing is held back. function downloadSelected(): void { if (!selectedSourceId || selectedActionable.length === 0) return - const n = enqueueItems(selectedSourceId, selectedActionable) + const n = enqueueItems(selectedSourceId, selectedActionable, enqueueKind) setSelected(new Set()) setBatchNote(`Queued ${n} download${n === 1 ? '' : 's'}.`) } function downloadPending(): void { if (!selectedSourceId || pendingItems.length === 0) return - const n = enqueueItems(selectedSourceId, pendingItems) + const n = enqueueItems(selectedSourceId, pendingItems, enqueueKind) setBatchNote(`Queued ${n} download${n === 1 ? '' : 's'}.`) } @@ -433,7 +439,7 @@ export function LibraryView(): React.JSX.Element { if (!selectedSourceId) return const toQueue = groupItems.filter(actionable) if (toQueue.length === 0) return - const n = enqueueItems(selectedSourceId, toQueue) + const n = enqueueItems(selectedSourceId, toQueue, enqueueKind) setBatchNote(`Queued ${n} download${n === 1 ? '' : 's'}.`) } @@ -634,6 +640,15 @@ export function LibraryView(): React.JSX.Element { {relTime(src.lastIndexedAt)}
+ + value={enqueueKind} + options={[ + { value: 'video', label: 'Video' }, + { value: 'audio', label: 'Audio' } + ]} + onChange={setEnqueueKind} + ariaLabel="Download as video or audio" + /> {selected.size > 0 ? ( <>