From a4d92b0759e67f8f257bc211315fa54d0378024f Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:46:59 +0000 Subject: [PATCH] feat(downloads): implement SequentialFileWriter for individual file downloads --- js/bulk-download-writer.ts | 29 ++++++++++++++++++ js/downloads.js | 61 +++++--------------------------------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/js/bulk-download-writer.ts b/js/bulk-download-writer.ts index 24a2367..581eaa2 100644 --- a/js/bulk-download-writer.ts +++ b/js/bulk-download-writer.ts @@ -41,6 +41,35 @@ export interface IBulkDownloadWriter { write(files: AsyncIterable): Promise; } +/** + * Triggers individual downloads for each file entry, one after another. + */ +export class SequentialFileWriter implements IBulkDownloadWriter { + constructor() {} + + async write(files: AsyncIterable): Promise { + for await (const file of files) { + const name = file.name?.split('/').pop(); + const ext = name?.split('.').pop().toLowerCase(); + + if (!name) { + console.warn('No name for file entry.', file); + continue; + } + + if (['m3u', 'm3u8', 'cue', 'jpg', 'png', 'nfo', 'json'].includes(ext)) { + continue; + } + + if (file.input instanceof Blob) { + triggerDownload(file.input, name); + } else { + triggerDownload(new Blob([file.input as BlobPart]), name); + } + } + } +} + /** * Streams a ZIP archive to a file via the File System Access API. * Prompts the user to choose a save location with showSaveFilePicker. diff --git a/js/downloads.js b/js/downloads.js index bcf5aea..0ef08d1 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -16,7 +16,13 @@ import { AbortError } from './errorTypes.ts'; import { lyricsSettings, bulkDownloadSettings, playlistSettings } from './storage.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { triggerDownload } from './download-utils.ts'; -import { ZipStreamWriter, ZipBlobWriter, ZipNeutralinoWriter, FolderPickerWriter } from './bulk-download-writer.ts'; +import { + ZipStreamWriter, + ZipBlobWriter, + ZipNeutralinoWriter, + FolderPickerWriter, + SequentialFileWriter, +} from './bulk-download-writer.ts'; import { FfmpegProgress } from './ffmpeg.types.js'; import { DownloadProgress, ProgressMessage, SegmentedDownloadProgress } from './progressEvents.js'; @@ -318,44 +324,6 @@ async function downloadTrackBlob(track, quality, api, signal = null, onProgress return { blob, extension }; } -async function bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification, coverBlob = null) { - const { abortController } = bulkDownloadTasks.get(notification); - const signal = abortController.signal; - - for (let i = 0; i < tracks.length; i++) { - if (signal.aborted) break; - const track = tracks[i]; - const trackTitle = getTrackTitle(track); - - updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); - - try { - const { blob, extension } = await downloadTrackBlob(track, quality, api, signal, null); - const filename = buildTrackFilename(track, quality, extension); - triggerDownload(blob, filename); - - if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { - try { - const lyricsData = await lyricsManager.fetchLyrics(track.id, track); - if (lyricsData) { - const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); - if (lrcContent) { - const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); - const lrcBlob = new Blob([lrcContent], { type: 'text/plain' }); - triggerDownload(lrcBlob, lrcFilename); - } - } - } catch { - // Silent fail for lyrics - } - } - } catch (err) { - if (err.name === 'AbortError') throw err; - console.error(`Failed to download track ${trackTitle}:`, err); - } - } -} - async function bulkDownload( tracks, folderName, @@ -548,7 +516,7 @@ async function createBulkWriter(folderName) { } } if (method === 'individual') { - return null; + return new SequentialFileWriter(); } // method === 'zip' (or folder picker unavailable as fallback) if (!forceZipBlob && hasFileSystemAccess) { @@ -586,9 +554,6 @@ async function startBulkDownload( type, metadata ); - } else { - // Individual sequential downloads - await bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification); } completeBulkDownload(notification, true); @@ -803,16 +768,6 @@ export async function downloadDiscography(artist, selectedReleases, api, quality if (writer) { await writer.write(yieldDiscography()); - } else { - // Individual sequential downloads for discography - for (let albumIndex = 0; albumIndex < selectedReleases.length; albumIndex++) { - if (signal.aborted) break; - const album = selectedReleases[albumIndex]; - updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title); - const { tracks: rawTracks } = await api.getAlbum(album.id); - const tracks = await annotateTracksWithDiscInfo(rawTracks, api); - await bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification); - } } completeBulkDownload(notification, true);