Fix bulk download edge cases and improve robustness
- FolderPickerWriter: throw AbortError on cancel instead of returning null - FolderPickerWriter: add try/catch with abort() to release file locks on failure - ZipNeutralinoWriter: move writeBinaryFile after response.body validation - bulkDownloadSettings: migrate legacy key and validate stored values - download-utils: catch ffmpeg cancellation via signal.aborted - downloads.js: use consistent Neutralino detection with bridge module - download-utils: use strict equality for flac extension check
This commit is contained in:
parent
a776e24aee
commit
b31be7dc80
4 changed files with 57 additions and 28 deletions
|
|
@ -98,11 +98,11 @@ export class ZipNeutralinoWriter implements IBulkDownloadWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { downloadZip } = await loadClientZip();
|
const { downloadZip } = await loadClientZip();
|
||||||
await bridge.filesystem.writeBinaryFile(savePath, new ArrayBuffer(0));
|
|
||||||
|
|
||||||
const response = downloadZip(files);
|
const response = downloadZip(files);
|
||||||
if (!response.body) throw new Error('ZIP response body is null');
|
if (!response.body) throw new Error('ZIP response body is null');
|
||||||
|
|
||||||
|
await bridge.filesystem.writeBinaryFile(savePath, new ArrayBuffer(0));
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
let receivedLength = 0;
|
let receivedLength = 0;
|
||||||
|
|
||||||
|
|
@ -138,10 +138,17 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
|
||||||
static async create(): Promise<FolderPickerWriter> {
|
static async create(): Promise<FolderPickerWriter> {
|
||||||
// showDirectoryPicker is part of the File System Access API (not yet in all TS DOM libs)
|
// showDirectoryPicker is part of the File System Access API (not yet in all TS DOM libs)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({
|
try {
|
||||||
mode: 'readwrite',
|
const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({
|
||||||
});
|
mode: 'readwrite',
|
||||||
return new FolderPickerWriter(dirHandle);
|
});
|
||||||
|
return new FolderPickerWriter(dirHandle);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new DOMException('User cancelled directory picker', 'AbortError');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async write(files: AsyncIterable<WriterEntry>): Promise<void> {
|
async write(files: AsyncIterable<WriterEntry>): Promise<void> {
|
||||||
|
|
@ -158,23 +165,25 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
|
||||||
const fileHandle = await currentDir.getFileHandle(filename, { create: true });
|
const fileHandle = await currentDir.getFileHandle(filename, { create: true });
|
||||||
const writable = await fileHandle.createWritable();
|
const writable = await fileHandle.createWritable();
|
||||||
|
|
||||||
const { input } = file;
|
try {
|
||||||
if (input instanceof Blob) {
|
const { input } = file;
|
||||||
await writable.write(input);
|
if (input instanceof Blob) {
|
||||||
} else if (typeof input === 'string') {
|
await writable.write(input);
|
||||||
await writable.write(new Blob([input], { type: 'text/plain' }));
|
} else if (typeof input === 'string') {
|
||||||
} else {
|
await writable.write(new Blob([input], { type: 'text/plain' }));
|
||||||
// ArrayBuffer or Uint8Array – wrap in a Blob to guarantee strict typing.
|
} else {
|
||||||
// Use byteOffset/byteLength so only the view's range is included, not the
|
const buf =
|
||||||
// whole backing ArrayBuffer (which may be larger due to pooling).
|
input instanceof Uint8Array
|
||||||
const buf =
|
? input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
|
||||||
input instanceof Uint8Array
|
: input;
|
||||||
? input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
|
await writable.write(new Blob([buf as ArrayBuffer]));
|
||||||
: input;
|
}
|
||||||
await writable.write(new Blob([buf as ArrayBuffer]));
|
|
||||||
}
|
|
||||||
|
|
||||||
await writable.close();
|
await writable.close();
|
||||||
|
} catch (error) {
|
||||||
|
await writable.abort();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export async function applyAudioPostProcessing(
|
||||||
|
|
||||||
if (await containerFmt?.needsTranscode(blob)) {
|
if (await containerFmt?.needsTranscode(blob)) {
|
||||||
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
||||||
} else if (extension == 'flac') {
|
} else if (extension === 'flac') {
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
blob = await rebuildFlacWithoutMetadata(blob);
|
||||||
} else {
|
} else {
|
||||||
blob = await ffmpegNewContainer(
|
blob = await ffmpegNewContainer(
|
||||||
|
|
@ -76,7 +76,7 @@ export async function applyAudioPostProcessing(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error)?.name === 'AbortError') {
|
if ((error as Error)?.name === 'AbortError' || signal?.aborted) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -669,7 +669,9 @@ async function bulkDownloadToZip(
|
||||||
* or null when individual sequential downloads should be used.
|
* or null when individual sequential downloads should be used.
|
||||||
*/
|
*/
|
||||||
async function createBulkWriter(folderName) {
|
async function createBulkWriter(folderName) {
|
||||||
const isNeutralino = window.NL_MODE === true;
|
const isNeutralino =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.NL_MODE || window.location.search.includes('mode=neutralino') || window.parent !== window);
|
||||||
const method = bulkDownloadSettings.getMethod();
|
const method = bulkDownloadSettings.getMethod();
|
||||||
const forceZipBlob = bulkDownloadSettings.shouldForceZipBlob();
|
const forceZipBlob = bulkDownloadSettings.shouldForceZipBlob();
|
||||||
const hasFileSystemAccess = 'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
|
const hasFileSystemAccess = 'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
|
||||||
|
|
@ -679,8 +681,14 @@ async function createBulkWriter(folderName) {
|
||||||
return new ZipNeutralinoWriter(folderName);
|
return new ZipNeutralinoWriter(folderName);
|
||||||
}
|
}
|
||||||
if (method === 'folder' && hasFolderPicker) {
|
if (method === 'folder' && hasFolderPicker) {
|
||||||
// FolderPickerWriter.create() throws AbortError if the user cancels
|
try {
|
||||||
return await FolderPickerWriter.create();
|
return await FolderPickerWriter.create();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (method === 'individual') {
|
if (method === 'individual') {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -637,11 +637,23 @@ export const trackDateSettings = {
|
||||||
export const bulkDownloadSettings = {
|
export const bulkDownloadSettings = {
|
||||||
METHOD_KEY: 'bulk-download-method',
|
METHOD_KEY: 'bulk-download-method',
|
||||||
FORCE_ZIP_BLOB_KEY: 'bulk-download-force-zip-blob',
|
FORCE_ZIP_BLOB_KEY: 'bulk-download-force-zip-blob',
|
||||||
|
LEGACY_INDIVIDUAL_KEY: 'force-individual-downloads',
|
||||||
|
VALID_METHODS: ['zip', 'folder', 'individual'],
|
||||||
|
|
||||||
/** Returns the selected bulk download method: 'zip' | 'folder' | 'individual' */
|
/** Returns the selected bulk download method: 'zip' | 'folder' | 'individual' */
|
||||||
getMethod() {
|
getMethod() {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(this.METHOD_KEY) || 'zip';
|
const stored = localStorage.getItem(this.METHOD_KEY);
|
||||||
|
if (stored && this.VALID_METHODS.includes(stored)) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
const legacy = localStorage.getItem(this.LEGACY_INDIVIDUAL_KEY);
|
||||||
|
if (legacy === 'true') {
|
||||||
|
localStorage.setItem(this.METHOD_KEY, 'individual');
|
||||||
|
localStorage.removeItem(this.LEGACY_INDIVIDUAL_KEY);
|
||||||
|
return 'individual';
|
||||||
|
}
|
||||||
|
return 'zip';
|
||||||
} catch {
|
} catch {
|
||||||
return 'zip';
|
return 'zip';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue