feat: extract duplicated download utilities from api.js and downloads.js into download-utils.ts

Co-authored-by: DanTheMan827 <790119+DanTheMan827@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-11 22:13:40 +00:00 committed by GitHub
parent 97bff01796
commit c9a1f49f23
3 changed files with 91 additions and 289 deletions

192
js/api.js
View file

@ -9,21 +9,15 @@ import {
getFullArtistString,
getMimeType,
} from './utils.js';
import { trackDateSettings, losslessContainerSettings } from './storage.js';
import { trackDateSettings } from './storage.js';
import { APICache } from './cache.js';
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
import { DashDownloader } from './dash-downloader.js';
import { HlsDownloader } from './hls-downloader.js';
import { MP3EncodingError } from './mp3-encoder.js';
import { ffmpeg, loadFfmpeg, FfmpegError } from './ffmpeg.js';
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
import {
isCustomFormat,
getCustomFormat,
transcodeWithCustomFormat,
getContainerFormat,
transcodeWithContainerFormat,
} from './ffmpegFormats.ts';
import { loadFfmpeg, FfmpegError } from './ffmpeg.js';
import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
import { isCustomFormat } from './ffmpegFormats.ts';
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
@ -1426,170 +1420,7 @@ export class LosslessAPI {
}
if (!isVideo) {
const coverBlobToEmbed = await prefetchPromises.coverFetch;
const extraFiles = [];
const ffmpegMetadataArgs = [];
if (coverBlobToEmbed) {
const coverBuffer = await coverBlobToEmbed.arrayBuffer();
const coverExt = getMimeType(new Uint8Array(coverBuffer)) === 'image/png' ? 'png' : 'jpg';
const coverName = `cover.${coverExt}`;
extraFiles.push({
name: coverName,
data: coverBuffer,
});
ffmpegMetadataArgs.push('-i', coverName);
}
if (track) {
ffmpegMetadataArgs.push(
'-metadata',
`title=${getTrackTitle(track)}`,
'-metadata',
`artist=${getFullArtistString(track)}`,
'-metadata',
`album=${track.album?.title || ''}`,
'-metadata',
`album_artist=${track.album?.artist?.name || track.artist?.name || ''}`
);
const trackNum = track.trackNumber;
if (trackNum) {
const totalTracks = track.album?.numberOfTracks;
ffmpegMetadataArgs.push(
'-metadata',
`track=${trackNum}${totalTracks ? `/${totalTracks}` : ''}`
);
}
const discNum = track.volumeNumber || track.discNumber;
if (discNum) {
ffmpegMetadataArgs.push('-metadata', `disc=${discNum}`);
}
const releaseDate = track.album?.releaseDate || track?.streamStartDate;
if (releaseDate) {
ffmpegMetadataArgs.push('-metadata', `date=${releaseDate.split('-')[0]}`);
}
}
// Transcode to custom format if requested
if (isCustomFormat(quality)) {
const format = getCustomFormat(quality);
if (format) {
try {
const args = [...ffmpegMetadataArgs, ...format.ffmpegArgs];
if (coverBlobToEmbed) {
args.push(
'-map',
'0:a',
'-map',
'1:v',
'-c:v',
'copy',
'-disposition:v:0',
'attached_pic'
);
}
blob = await ffmpeg(
blob,
{ args },
format.outputFilename,
format.outputMime,
onProgress,
options.signal,
extraFiles
);
} catch (encodingError) {
if (onProgress) {
onProgress({
stage: 'error',
message: `Encoding failed: ${encodingError.message}`,
});
}
throw encodingError;
}
}
}
if (quality.endsWith('LOSSLESS')) {
try {
const containerType = losslessContainerSettings.getContainer();
const containerFmt = getContainerFormat(containerType);
if (containerFmt && containerType !== 'nochange') {
if (await containerFmt.needsTranscode(blob)) {
const args = [...ffmpegMetadataArgs, ...containerFmt.ffmpegArgs];
if (coverBlobToEmbed) {
args.push(
'-map',
'0:a',
'-map',
'1:v',
'-c:v',
'copy',
'-disposition:v:0',
'attached_pic'
);
}
blob = await ffmpeg(
blob,
{ args },
containerFmt.outputFilename,
containerFmt.outputMime,
onProgress,
options.signal,
extraFiles
);
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
blob = await rebuildFlacWithoutMetadata(blob);
}
} else {
const actualExtension = await getExtensionFromBlob(blob);
if (actualExtension === 'm4a' || actualExtension === 'mp4') {
try {
const ffmpegArgs = [...ffmpegMetadataArgs];
ffmpegArgs.push('-map', '0:a');
if (coverBlobToEmbed) {
ffmpegArgs.push(
'-map',
'1:v',
'-c:v',
'copy',
'-disposition:v:0',
'attached_pic'
);
}
ffmpegArgs.push('-c:a', 'copy', '-movflags', '+faststart', '-strict', '-2');
const remuxedBlob = await ffmpeg(
blob,
{ args: ffmpegArgs },
'output.mp4',
'audio/mp4',
onProgress,
options.signal,
extraFiles
);
if (remuxedBlob) {
blob = remuxedBlob;
}
} catch (e) {
console.warn('Failed to remux hi-res M4A, proceeding with original:', e);
}
}
}
} catch (error) {
if (error?.name === 'AbortError') {
throw error;
}
console.error('Lossless container conversion failed:', error);
}
}
blob = await applyAudioPostProcessing(blob, quality, onProgress, options.signal);
// Add metadata if track information is provided
if (track) {
@ -1673,7 +1504,7 @@ export class LosslessAPI {
finalFilename = filename.replace(/\.[^.]+$/, `.${detectedExtension}`);
}
this.triggerDownload(blob, finalFilename);
triggerDownload(blob, finalFilename);
return blob;
} catch (error) {
if (error.name === 'AbortError') {
@ -1694,17 +1525,6 @@ export class LosslessAPI {
}
}
triggerDownload(blob, filename) {
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);
}
getCoverUrl(id, size = '320') {
if (!id) {
return `https://picsum.photos/seed/${Math.random()}/${size}`;

79
js/download-utils.ts Normal file
View file

@ -0,0 +1,79 @@
import { losslessContainerSettings } from './storage';
import { rebuildFlacWithoutMetadata } from './metadata.flac';
import { getExtensionFromBlob } from './utils';
import {
type ProgressEvent,
isCustomFormat,
getCustomFormat,
transcodeWithCustomFormat,
getContainerFormat,
transcodeWithContainerFormat,
} from './ffmpegFormats';
/**
* 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());
if (containerFmt) {
if (await containerFmt.needsTranscode(blob)) {
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
blob = await rebuildFlacWithoutMetadata(blob);
}
}
} catch (error) {
if ((error as Error)?.name === 'AbortError') {
throw error;
}
console.error('Lossless container conversion failed:', error);
}
}
return blob;
}

View file

@ -14,19 +14,13 @@ import {
getFullArtistString,
getMimeType,
} from './utils.js';
import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js';
import { lyricsSettings, bulkDownloadSettings, playlistSettings } from './storage.js';
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
import { DashDownloader } from './dash-downloader.js';
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
import {
isCustomFormat,
getCustomFormat,
transcodeWithCustomFormat,
getContainerFormat,
transcodeWithContainerFormat,
} from './ffmpegFormats.ts';
import { loadFfmpeg } from './ffmpeg.js';
import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts';
import { isCustomFormat } from './ffmpegFormats.ts';
const downloadTasks = new Map();
const bulkDownloadTasks = new Map();
@ -464,88 +458,8 @@ async function downloadTrackBlob(
}
}
// Transcode to custom format if requested
if (isCustomFormat(quality)) {
const format = getCustomFormat(quality);
if (format) {
const args = [...ffmpegMetadataArgs, ...format.ffmpegArgs];
if (coverBlobToEmbed) {
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
}
blob = await ffmpeg(
blob,
{ args },
format.outputFilename,
format.outputMime,
onProgress,
signal,
extraFiles
);
}
}
if (quality.endsWith('LOSSLESS')) {
try {
const containerType = losslessContainerSettings.getContainer();
const containerFmt = getContainerFormat(containerType);
if (containerFmt && containerType !== 'nochange') {
if (await containerFmt.needsTranscode(blob)) {
const args = [...ffmpegMetadataArgs, ...containerFmt.ffmpegArgs];
if (coverBlobToEmbed) {
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
}
blob = await ffmpeg(
blob,
{ args },
containerFmt.outputFilename,
containerFmt.outputMime,
onProgress,
signal,
extraFiles
);
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
blob = await rebuildFlacWithoutMetadata(blob);
}
} else {
const actualExtension = await getExtensionFromBlob(blob);
if (actualExtension === 'm4a' || actualExtension === 'mp4') {
try {
const ffmpegArgs = [...ffmpegMetadataArgs];
ffmpegArgs.push('-map', '0:a');
if (coverBlobToEmbed) {
ffmpegArgs.push('-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
}
ffmpegArgs.push('-c:a', 'copy', '-movflags', '+faststart', '-strict', '-2');
const remuxedBlob = await ffmpeg(
blob,
{ args: ffmpegArgs },
'output.mp4',
'audio/mp4',
onProgress,
signal,
extraFiles
);
if (remuxedBlob) {
blob = remuxedBlob;
}
} catch (e) {
console.warn('Failed to remux hi-res M4A, proceeding with original:', e);
}
}
}
} catch (error) {
if (error?.name === 'AbortError') {
throw error;
}
console.error('Lossless container conversion failed:', error);
}
}
// Apply audio post-processing (custom format transcoding + lossless container conversion)
blob = await applyAudioPostProcessing(blob, quality, onProgress, signal);
// Detect actual format from blob signature BEFORE adding metadata
const extension = await getExtensionFromBlob(blob);
@ -556,17 +470,6 @@ async function downloadTrackBlob(
return { blob, extension };
}
function triggerDownload(blob, filename) {
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);
}
async function bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification, coverBlob = null) {
const { abortController } = bulkDownloadTasks.get(notification);
const signal = abortController.signal;