fix(downloading): hi-res M4A's having no embedded covers (FUCK YOU TAGLIB)
This commit is contained in:
parent
0ed82f586c
commit
30b2e7d445
8 changed files with 280 additions and 45 deletions
108
js/api.js
108
js/api.js
|
|
@ -5,6 +5,9 @@ import {
|
||||||
delay,
|
delay,
|
||||||
isTrackUnavailable,
|
isTrackUnavailable,
|
||||||
getExtensionFromBlob,
|
getExtensionFromBlob,
|
||||||
|
getTrackTitle,
|
||||||
|
getFullArtistString,
|
||||||
|
getMimeType,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { trackDateSettings, losslessContainerSettings } from './storage.js';
|
import { trackDateSettings, losslessContainerSettings } from './storage.js';
|
||||||
import { APICache } from './cache.js';
|
import { APICache } from './cache.js';
|
||||||
|
|
@ -12,7 +15,7 @@ import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { HlsDownloader } from './hls-downloader.js';
|
import { HlsDownloader } from './hls-downloader.js';
|
||||||
import { MP3EncodingError } from './mp3-encoder.js';
|
import { MP3EncodingError } from './mp3-encoder.js';
|
||||||
import { loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
import { ffmpeg, loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
||||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||||
import {
|
import {
|
||||||
isCustomFormat,
|
isCustomFormat,
|
||||||
|
|
@ -1423,12 +1426,65 @@ export class LosslessAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isVideo) {
|
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
|
// Transcode to custom format if requested
|
||||||
if (isCustomFormat(quality)) {
|
if (isCustomFormat(quality)) {
|
||||||
const format = getCustomFormat(quality);
|
const format = getCustomFormat(quality);
|
||||||
if (format) {
|
if (format) {
|
||||||
try {
|
try {
|
||||||
blob = await transcodeWithCustomFormat(blob, format, onProgress, options.signal);
|
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) {
|
} catch (encodingError) {
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
onProgress({
|
onProgress({
|
||||||
|
|
@ -1443,18 +1499,56 @@ export class LosslessAPI {
|
||||||
|
|
||||||
if (quality.endsWith('LOSSLESS')) {
|
if (quality.endsWith('LOSSLESS')) {
|
||||||
try {
|
try {
|
||||||
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
const containerType = losslessContainerSettings.getContainer();
|
||||||
if (containerFmt) {
|
const containerFmt = getContainerFormat(containerType);
|
||||||
|
|
||||||
|
if (containerFmt && containerType !== 'nochange') {
|
||||||
if (await containerFmt.needsTranscode(blob)) {
|
if (await containerFmt.needsTranscode(blob)) {
|
||||||
blob = await transcodeWithContainerFormat(
|
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,
|
blob,
|
||||||
containerFmt,
|
{ args },
|
||||||
|
containerFmt.outputFilename,
|
||||||
|
containerFmt.outputMime,
|
||||||
onProgress,
|
onProgress,
|
||||||
options.signal
|
options.signal,
|
||||||
|
extraFiles
|
||||||
);
|
);
|
||||||
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
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) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
|
|
|
||||||
108
js/downloads.js
108
js/downloads.js
|
|
@ -11,13 +11,15 @@ import {
|
||||||
getExtensionFromBlob,
|
getExtensionFromBlob,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
getTrackDiscNumber,
|
getTrackDiscNumber,
|
||||||
|
getFullArtistString,
|
||||||
|
getMimeType,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js';
|
import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js';
|
||||||
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
||||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
||||||
import { loadFfmpeg } from './ffmpeg.js';
|
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
|
||||||
import {
|
import {
|
||||||
isCustomFormat,
|
isCustomFormat,
|
||||||
getCustomFormat,
|
getCustomFormat,
|
||||||
|
|
@ -418,23 +420,119 @@ async function downloadTrackBlob(
|
||||||
blob = await response.blob();
|
blob = await response.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (enrichedTrack) {
|
||||||
|
ffmpegMetadataArgs.push(
|
||||||
|
'-metadata', `title=${getTrackTitle(enrichedTrack)}`,
|
||||||
|
'-metadata', `artist=${getFullArtistString(enrichedTrack)}`,
|
||||||
|
'-metadata', `album=${enrichedTrack.album?.title || ''}`,
|
||||||
|
'-metadata', `album_artist=${enrichedTrack.album?.artist?.name || enrichedTrack.artist?.name || ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const trackNum = enrichedTrack.trackNumber;
|
||||||
|
if (trackNum) {
|
||||||
|
const totalTracks = enrichedTrack.album?.numberOfTracks;
|
||||||
|
ffmpegMetadataArgs.push('-metadata', `track=${trackNum}${totalTracks ? `/${totalTracks}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const discNum = enrichedTrack.volumeNumber || enrichedTrack.discNumber;
|
||||||
|
if (discNum) {
|
||||||
|
ffmpegMetadataArgs.push('-metadata', `disc=${discNum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseDate = enrichedTrack.album?.releaseDate || enrichedTrack?.streamStartDate;
|
||||||
|
if (releaseDate) {
|
||||||
|
ffmpegMetadataArgs.push('-metadata', `date=${releaseDate.split('-')[0]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transcode to custom format if requested
|
// Transcode to custom format if requested
|
||||||
if (isCustomFormat(quality)) {
|
if (isCustomFormat(quality)) {
|
||||||
const format = getCustomFormat(quality);
|
const format = getCustomFormat(quality);
|
||||||
if (format) {
|
if (format) {
|
||||||
blob = await transcodeWithCustomFormat(blob, format, onProgress || (() => undefined), signal);
|
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')) {
|
if (quality.endsWith('LOSSLESS')) {
|
||||||
try {
|
try {
|
||||||
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
const containerType = losslessContainerSettings.getContainer();
|
||||||
if (containerFmt) {
|
const containerFmt = getContainerFormat(containerType);
|
||||||
|
|
||||||
|
if (containerFmt && containerType !== 'nochange') {
|
||||||
if (await containerFmt.needsTranscode(blob)) {
|
if (await containerFmt.needsTranscode(blob)) {
|
||||||
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
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') {
|
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
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) {
|
} catch (error) {
|
||||||
if (error?.name === 'AbortError') {
|
if (error?.name === 'AbortError') {
|
||||||
|
|
|
||||||
23
js/ffmpeg.js
23
js/ffmpeg.js
|
|
@ -32,9 +32,10 @@ async function ffmpegWorker(
|
||||||
outputName = 'output',
|
outputName = 'output',
|
||||||
outputMime = 'application/octet-stream',
|
outputMime = 'application/octet-stream',
|
||||||
onProgress = null,
|
onProgress = null,
|
||||||
signal = null
|
signal = null,
|
||||||
|
extraFiles = []
|
||||||
) {
|
) {
|
||||||
const audioData = await audioBlob.arrayBuffer();
|
const audioData = audioBlob ? await audioBlob.arrayBuffer() : null;
|
||||||
const assets = loadFfmpeg();
|
const assets = loadFfmpeg();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
@ -79,9 +80,20 @@ async function ffmpegWorker(
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const transferables = [];
|
||||||
|
if (audioData) transferables.push(audioData);
|
||||||
|
for (const f of extraFiles) {
|
||||||
|
if (f.data instanceof ArrayBuffer) {
|
||||||
|
transferables.push(f.data);
|
||||||
|
} else if (f.data.buffer instanceof ArrayBuffer) {
|
||||||
|
transferables.push(f.data.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
worker.postMessage(
|
worker.postMessage(
|
||||||
{
|
{
|
||||||
audioData,
|
audioData,
|
||||||
|
extraFiles,
|
||||||
...args,
|
...args,
|
||||||
output: {
|
output: {
|
||||||
name: outputName,
|
name: outputName,
|
||||||
|
|
@ -89,7 +101,7 @@ async function ffmpegWorker(
|
||||||
},
|
},
|
||||||
loadOptions: await assets,
|
loadOptions: await assets,
|
||||||
},
|
},
|
||||||
[audioData]
|
transferables
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
@ -101,12 +113,13 @@ export async function ffmpeg(
|
||||||
outputName = 'output',
|
outputName = 'output',
|
||||||
outputMime = 'application/octet-stream',
|
outputMime = 'application/octet-stream',
|
||||||
onProgress = null,
|
onProgress = null,
|
||||||
signal = null
|
signal = null,
|
||||||
|
extraFiles = []
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Use Web Worker for non-blocking FFmpeg encoding
|
// Use Web Worker for non-blocking FFmpeg encoding
|
||||||
if (typeof Worker !== 'undefined') {
|
if (typeof Worker !== 'undefined') {
|
||||||
return await ffmpegWorker(audioBlob, args, outputName, outputMime, onProgress, signal);
|
return await ffmpegWorker(audioBlob, args, outputName, outputMime, onProgress, signal, extraFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FfmpegError('Web Workers are required for FFMPEG');
|
throw new FfmpegError('Web Workers are required for FFMPEG');
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ async function loadFFmpeg(loadOptions = {}) {
|
||||||
self.onmessage = async (e) => {
|
self.onmessage = async (e) => {
|
||||||
const {
|
const {
|
||||||
audioData,
|
audioData,
|
||||||
|
extraFiles = [],
|
||||||
args = [],
|
args = [],
|
||||||
output = {
|
output = {
|
||||||
name: 'output',
|
name: 'output',
|
||||||
|
|
@ -109,42 +110,40 @@ self.onmessage = async (e) => {
|
||||||
} = e.data;
|
} = e.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(loadOptions);
|
|
||||||
await loadFFmpeg(loadOptions);
|
await loadFFmpeg(loadOptions);
|
||||||
|
|
||||||
self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage, progress: 0.0 });
|
self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage, progress: 0.0 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Write input file to FFmpeg virtual filesystem
|
if (audioData) {
|
||||||
await ffmpeg.writeFile('input', new Uint8Array(audioData));
|
await ffmpeg.writeFile('input', new Uint8Array(audioData));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of extraFiles) {
|
||||||
|
await ffmpeg.writeFile(file.name, new Uint8Array(file.data));
|
||||||
|
}
|
||||||
|
|
||||||
const ffmpegArgs = ['-i', 'input', ...args, output.name];
|
const ffmpegArgs = ['-i', 'input', ...args, output.name];
|
||||||
|
self.postMessage({ type: 'log', message: `FFmpeg command: ffmpeg ${ffmpegArgs.join(' ')}` });
|
||||||
|
|
||||||
// Log the exact FFmpeg command being run for debugging.
|
const exitCode = await ffmpeg.exec(ffmpegArgs);
|
||||||
self.postMessage({ type: 'log', message: `Running with args: ${ffmpegArgs.join(' ')}` });
|
|
||||||
|
|
||||||
// Run FFMPEG with the provided arguments.
|
if (exitCode !== 0) {
|
||||||
await ffmpeg.exec(ffmpegArgs);
|
throw new Error(`FFmpeg failed with exit code ${exitCode}.`);
|
||||||
|
}
|
||||||
|
|
||||||
self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage, progress: 100.0 });
|
self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage, progress: 100.0 });
|
||||||
|
|
||||||
// Read output file - use Uint8Array directly to avoid extra bytes from ArrayBuffer
|
|
||||||
const data = await ffmpeg.readFile(output.name);
|
const data = await ffmpeg.readFile(output.name);
|
||||||
const outputBlob = new Blob([data], { type: output.mime });
|
const outputBlob = new Blob([data], { type: output.mime });
|
||||||
|
|
||||||
self.postMessage({ type: 'complete', blob: outputBlob });
|
self.postMessage({ type: 'complete', blob: outputBlob });
|
||||||
} finally {
|
} finally {
|
||||||
// Always cleanup virtual filesystem files
|
try { if (audioData) await ffmpeg.deleteFile('input'); } catch {}
|
||||||
try {
|
for (const file of extraFiles) {
|
||||||
await ffmpeg.deleteFile('input');
|
try { await ffmpeg.deleteFile(file.name); } catch {}
|
||||||
} catch {
|
|
||||||
// File may not exist if writeFile failed
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await ffmpeg.deleteFile(output.name);
|
|
||||||
} catch {
|
|
||||||
// File may not exist if exec failed
|
|
||||||
}
|
}
|
||||||
|
try { await ffmpeg.deleteFile(output.name); } catch {}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({ type: 'error', message: error.message });
|
self.postMessage({ type: 'error', message: error.message });
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,18 @@ export async function transcodeWithCustomFormat(
|
||||||
audioBlob: Blob,
|
audioBlob: Blob,
|
||||||
format: CustomFormat,
|
format: CustomFormat,
|
||||||
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||||
signal: AbortSignal | null = null
|
signal: AbortSignal | null = null,
|
||||||
|
extraFiles: any[] = []
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
return ffmpeg(
|
||||||
|
audioBlob,
|
||||||
|
{ args: format.ffmpegArgs },
|
||||||
|
format.outputFilename,
|
||||||
|
format.outputMime,
|
||||||
|
onProgress,
|
||||||
|
signal,
|
||||||
|
extraFiles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -223,7 +232,16 @@ export async function transcodeWithContainerFormat(
|
||||||
audioBlob: Blob,
|
audioBlob: Blob,
|
||||||
format: ContainerFormat,
|
format: ContainerFormat,
|
||||||
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||||
signal: AbortSignal | null = null
|
signal: AbortSignal | null = null,
|
||||||
|
extraFiles: any[] = []
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
return ffmpeg(
|
||||||
|
audioBlob,
|
||||||
|
{ args: format.ffmpegArgs },
|
||||||
|
format.outputFilename,
|
||||||
|
format.outputMime,
|
||||||
|
onProgress,
|
||||||
|
signal,
|
||||||
|
extraFiles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
getMimeType,
|
getMimeType,
|
||||||
getTrackCoverId,
|
getTrackCoverId,
|
||||||
getTrackDiscNumber,
|
getTrackDiscNumber,
|
||||||
|
getExtensionFromBlob,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
|
import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
|
||||||
import { doTimed, doTimedAsync } from './doTimed.ts';
|
import { doTimed, doTimedAsync } from './doTimed.ts';
|
||||||
|
|
@ -42,10 +43,18 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
||||||
const { coverFetch, lyricsFetch } = prefetchPromises;
|
const { coverFetch, lyricsFetch } = prefetchPromises;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("./taglib.types.ts").TagLibMetadata}
|
* @type {import("./taglib.worker.ts").TagLibMetadata}
|
||||||
*/
|
*/
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
|
const detectedExt = await getExtensionFromBlob(audioBlob);
|
||||||
|
const isM4A = detectedExt === 'm4a' || detectedExt === 'mp4';
|
||||||
|
|
||||||
|
if (isM4A) {
|
||||||
|
console.log('Skipping TagLib for M4A (handled by FFmpeg)');
|
||||||
|
return audioBlob;
|
||||||
|
}
|
||||||
|
|
||||||
const audioBuffer = await doTimedAsync('Get audio array buffer', () => audioBlob.arrayBuffer());
|
const audioBuffer = await doTimedAsync('Get audio array buffer', () => audioBlob.arrayBuffer());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -55,8 +64,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
||||||
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
||||||
data.trackNumber = track.trackNumber;
|
data.trackNumber = track.trackNumber;
|
||||||
data.discNumber = track.volumeNumber ?? track.discNumber;
|
data.discNumber = track.volumeNumber ?? track.discNumber;
|
||||||
data.totalTracks = track.album.numberOfTracksOnDisc ?? track.album.numberOfTracks;
|
data.totalTracks = track.album.numberOfTracks;
|
||||||
data.totalDiscs = track.album.totalDiscs;
|
|
||||||
data.copyright = track.copyright;
|
data.copyright = track.copyright;
|
||||||
data.isrc = track.isrc;
|
data.isrc = track.isrc;
|
||||||
data.explicit = Boolean(track.explicit);
|
data.explicit = Boolean(track.explicit);
|
||||||
|
|
@ -96,9 +104,9 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
||||||
try {
|
try {
|
||||||
if (track.album?.cover) {
|
if (track.album?.cover) {
|
||||||
const coverBlob = await coverFetch;
|
const coverBlob = await coverFetch;
|
||||||
const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer());
|
|
||||||
|
|
||||||
if (coverBlob) {
|
if (coverBlob) {
|
||||||
|
const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer());
|
||||||
data.cover = {
|
data.cover = {
|
||||||
data: coverBuffer,
|
data: coverBuffer,
|
||||||
type: getMimeType(coverBuffer),
|
type: getMimeType(coverBuffer),
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import { getButterchurnPresets } from './visualizers/butterchurn.js';
|
||||||
import { db } from './db.js';
|
import { db } from './db.js';
|
||||||
import { authManager } from './accounts/auth.js';
|
import { authManager } from './accounts/auth.js';
|
||||||
import { syncManager } from './accounts/pocketbase.js';
|
import { syncManager } from './accounts/pocketbase.js';
|
||||||
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
|
|
||||||
import { containerFormats, customFormats } from './ffmpegFormats.ts';
|
import { containerFormats, customFormats } from './ffmpegFormats.ts';
|
||||||
|
|
||||||
export function initializeSettings(scrobbler, player, api, ui) {
|
export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,13 @@ export async function addMetadataWithTagLib(
|
||||||
};
|
};
|
||||||
worker.onerror = reject;
|
worker.onerror = reject;
|
||||||
worker.onmessageerror = reject;
|
worker.onmessageerror = reject;
|
||||||
worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }, [audioData.buffer]);
|
|
||||||
|
const transferables: Transferable[] = [audioData.buffer];
|
||||||
|
if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) {
|
||||||
|
transferables.push((data as any).cover.data.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }, transferables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue