kv-music/js/download-utils.ts
edideaur b31be7dc80 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
2026-03-12 19:35:23 +00:00

88 lines
2.9 KiB
TypeScript

import { losslessContainerSettings } from './storage';
import { getExtensionFromBlob } from './utils';
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
import {
type ProgressEvent,
isCustomFormat,
getCustomFormat,
transcodeWithCustomFormat,
getContainerFormat,
transcodeWithContainerFormat,
} from './ffmpegFormats';
import { ffmpegNewContainer } from './ffmpeg';
/**
* Triggers a browser file download for the given blob.
*/
export function triggerDownload(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Applies audio post-processing to a blob:
* 1. Transcodes to a custom ffmpeg format if `quality` identifies one.
* 2. Re-muxes to the user-selected lossless container when the quality is
* a lossless tier (quality ends with "LOSSLESS").
*
* Returns the (possibly transformed) blob.
*/
export async function applyAudioPostProcessing(
blob: Blob,
quality: string,
onProgress: ((progress: ProgressEvent) => void) | null = null,
signal: AbortSignal | null = null
): Promise<Blob> {
// Transcode to custom format if requested
if (isCustomFormat(quality)) {
const format = getCustomFormat(quality);
if (format) {
try {
blob = await transcodeWithCustomFormat(blob, format, onProgress, signal);
} catch (encodingError) {
if (onProgress) {
onProgress({
stage: 'error',
message: `Encoding failed: ${(encodingError as Error).message}`,
});
}
throw encodingError;
}
}
}
if (quality.endsWith('LOSSLESS')) {
try {
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
const extension = await getExtensionFromBlob(blob);
if (await containerFmt?.needsTranscode(blob)) {
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
} else if (extension === 'flac') {
blob = await rebuildFlacWithoutMetadata(blob);
} else {
blob = await ffmpegNewContainer(
blob,
extension == 'm4a' ? 'mp4' : extension,
blob.type,
onProgress,
signal
);
}
} catch (error) {
if ((error as Error)?.name === 'AbortError' || signal?.aborted) {
throw error;
}
console.error('Lossless container conversion failed:', error);
}
}
return blob;
}