diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 96aca81..3e80479 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -373,6 +373,13 @@ export const AddDownloadsModal = () => { break; } requestSet.add(requestKey); + if (row.isPlaylist) { + // Invalidate stale playlist requests before any asynchronous settings, + // keychain, or network work can yield. Otherwise an old request can + // become the latest request again after the URL or browser context + // has already changed. + latestPlaylistRequestRef.current.set(row.sourceUrl, requestKey); + } void (async () => { try { @@ -411,7 +418,6 @@ export const AddDownloadsModal = () => { if (row.isPlaylist) { if (playlistExpansions[row.sourceUrl]) return; - latestPlaylistRequestRef.current.set(row.sourceUrl, requestKey); const playlistData = await fetchMediaPlaylistMetadataDeduped({ ...mediaMetadataArgs, url: contextUrl @@ -1047,11 +1053,11 @@ export const AddDownloadsModal = () => {
{/* Main Content Split */} -
+
{/* Left Column: URLs and Preview */}
-
+
@@ -1106,18 +1112,18 @@ export const AddDownloadsModal = () => { !i.sizeBytes).length} icon={FileText} color="text-purple-500" />
-
+
Preview
-
+
File
Size
Status
-
+
{parsedItems.length === 0 ? (
No links added yet. diff --git a/src/index.css b/src/index.css index dea3f64..0410e4e 100644 --- a/src/index.css +++ b/src/index.css @@ -545,6 +545,7 @@ html[data-list-density="relaxed"] { .add-download-left { min-width: 0; min-height: 0; + overflow: hidden; background: radial-gradient(circle at 18% 0%, hsl(var(--accent-color) / 0.055), transparent 34%), hsl(var(--main-bg) / 0.56); @@ -643,6 +644,8 @@ html[data-list-density="relaxed"] { } .add-download-preview { + min-width: 0; + min-height: 0; border: 1px solid hsl(var(--border-modal)); border-radius: 11px; background: hsl(var(--bg-input) / 0.35); @@ -650,11 +653,13 @@ html[data-list-density="relaxed"] { } .add-download-preview-header { + flex-shrink: 0; border-bottom: 1px solid hsl(var(--border-modal)); background: hsl(var(--surface-raised) / 0.66); } .add-download-preview-row { + flex-shrink: 0; transition: background-color 100ms ease, border-color 100ms ease, @@ -1780,6 +1785,7 @@ html[data-list-density="relaxed"] { } .download-table-header { + flex-shrink: 0; height: var(--download-header-height); display: grid; align-items: center; @@ -1882,12 +1888,14 @@ body.is-resizing .column-resize-handle:hover::after { .download-table-list { height: 100%; + min-height: 0; overflow: auto; display: flex; flex-direction: column; } .download-row { + flex: 0 0 var(--download-row-height); height: var(--download-row-height); display: grid; align-items: center; diff --git a/src/utils/addDownloadMetadata.test.ts b/src/utils/addDownloadMetadata.test.ts index ef65bfe..6c7b707 100644 --- a/src/utils/addDownloadMetadata.test.ts +++ b/src/utils/addDownloadMetadata.test.ts @@ -293,7 +293,10 @@ describe('add download metadata workflow', () => { const existing = row({ file: 'old-name.zip', requestContextVersion: 1, - generation: 2 + generation: 2, + size: '10 MB', + sizeBytes: 10, + resumable: true }); const refreshed = reconcileDownloadRows( @@ -310,7 +313,47 @@ describe('add download metadata workflow', () => { file: 'new-name.zip', status: 'loading', generation: 3, - requestContextVersion: 2 + requestContextVersion: 2, + size: undefined, + sizeBytes: undefined, + resumable: undefined + }); + }); + + it('drops stale playlist provenance when an entry remains after its playlist is removed', () => { + const videoUrl = 'https://www.youtube.com/watch?v=one'; + const playlistUrl = 'https://www.youtube.com/playlist?list=PL123'; + const existing = row({ + sourceUrl: videoUrl, + downloadUrl: videoUrl, + file: '001 - First.mp4', + status: 'ready', + generation: 3, + isMedia: true, + playlistSourceUrl: playlistUrl, + playlistTitle: 'Example playlist', + playlistIndex: 1, + playlistCount: 2, + playlistEntryTitle: 'First', + requestContextVersion: 7, + size: '10 MB', + sizeBytes: 10, + resumable: true + }); + + const rows = reconcileDownloadRows(videoUrl, [existing]); + + expect(rows[0]).toMatchObject({ + file: 'watch', + status: 'loading', + generation: 4, + isMedia: true, + playlistSourceUrl: undefined, + playlistIndex: undefined, + size: undefined, + sizeBytes: undefined, + resumable: undefined, + requestContextVersion: undefined }); }); @@ -425,6 +468,10 @@ describe('add download metadata workflow', () => { row({ status: 'metadata-error', selected: false }), row({ id: 'ready', status: 'ready' }) ])).toContain('Ready to add 1 download'); + expect(metadataSummaryMessage([ + row({ status: 'metadata-error' }), + row({ id: 'skipped', status: 'ready', selected: false }) + ])).toContain('can still be added'); }); it('keeps failed media routing without a format selector', () => { diff --git a/src/utils/addDownloadMetadata.ts b/src/utils/addDownloadMetadata.ts index 1fdf626..bd20611 100644 --- a/src/utils/addDownloadMetadata.ts +++ b/src/utils/addDownloadMetadata.ts @@ -182,21 +182,39 @@ export const reconcileDownloadRows = ( const requestContextVersion = input.requestContextVersion; const contextChanged = requestContextVersion !== undefined && requestContextVersion !== preserved.requestContextVersion; - if ((forcedMedia && !preserved.isMedia) || contextChanged) { + const playlistContextChanged = preserved.playlistSourceUrl !== input.playlistSourceUrl + || preserved.playlistTitle !== input.playlistTitle + || preserved.playlistIndex !== input.playlistIndex + || preserved.playlistCount !== input.playlistCount + || preserved.playlistEntryTitle !== input.playlistEntryTitle; + if ((forcedMedia && !preserved.isMedia) || contextChanged || playlistContextChanged) { const requestedFilename = input.playlistSourceUrl ? `${playlistFilePrefix(input.playlistIndex, input.playlistCount)}${input.playlistEntryTitle || 'video'}` : requestFilenames[input.sourceUrl]; return { ...preserved, - file: contextChanged + file: contextChanged || playlistContextChanged ? canonicalizeDownloadFileName(requestedFilename || fileNameFromUrl(input.sourceUrl)) : preserved.file, status: 'loading', generation: preserved.generation + 1, requestContextVersion, - isMedia: preserved.isMedia || forcedMedia, - formats: preserved.isMedia || forcedMedia ? undefined : preserved.formats, - selectedFormat: preserved.isMedia || forcedMedia ? undefined : preserved.selectedFormat, + isMedia: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl), + size: undefined, + sizeBytes: undefined, + resumable: undefined, + formats: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl) + ? undefined + : preserved.formats, + selectedFormat: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl) + ? undefined + : preserved.selectedFormat, + isPlaylist: input.isPlaylist, + playlistSourceUrl: input.playlistSourceUrl, + playlistTitle: input.playlistTitle, + playlistIndex: input.playlistIndex, + playlistCount: input.playlistCount, + playlistEntryTitle: input.playlistEntryTitle, playlistError: undefined }; } @@ -361,7 +379,7 @@ export const metadataSummaryMessage = (rows: AddDownloadDraftRow[]): string => { if (failedMedia > 0) { return `Media metadata is unavailable for ${failedMedia} item${failedMedia === 1 ? '' : 's'}. Refresh metadata before adding.`; } - if (failed === rows.length) { + if (failed === selectedRows.length) { return 'Metadata is unavailable. Downloads can still be added using fallback details.'; } if (failed > 0) {