cc
This commit is contained in:
parent
51d1b3bdce
commit
f990cb1fcc
3 changed files with 0 additions and 231 deletions
210
js/metadata.js
210
js/metadata.js
|
|
@ -1,210 +0,0 @@
|
||||||
export class MetadataEmbedder {
|
|
||||||
constructor() {
|
|
||||||
this.ffmpegLoaded = false;
|
|
||||||
this.ffmpeg = null;
|
|
||||||
this.fetchFile = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFFmpeg() {
|
|
||||||
if (this.ffmpegLoaded) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[FFmpeg] Loading FFmpeg...');
|
|
||||||
|
|
||||||
if (typeof FFmpegWASM === 'undefined' || typeof FFmpegUtil === 'undefined') {
|
|
||||||
throw new Error('FFmpeg libraries not loaded. Please check your internet connection.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { FFmpeg } = FFmpegWASM;
|
|
||||||
const { fetchFile } = FFmpegUtil;
|
|
||||||
|
|
||||||
this.ffmpeg = new FFmpeg();
|
|
||||||
this.fetchFile = fetchFile;
|
|
||||||
|
|
||||||
this.ffmpeg.on('log', ({ message }) => {
|
|
||||||
console.log('[FFmpeg]', message);
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseURL = window.location.origin + '/ffmpeg';
|
|
||||||
|
|
||||||
await this.ffmpeg.load({
|
|
||||||
coreURL: `${baseURL}/ffmpeg-core.js`,
|
|
||||||
wasmURL: `${baseURL}/ffmpeg-core.wasm`
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ffmpegLoaded = true;
|
|
||||||
console.log('[FFmpeg] Loaded successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[FFmpeg] Failed to load:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async embedMetadata(audioBlob, track, coverImageUrl, onProgress) {
|
|
||||||
console.log('[Metadata] Starting embedding for:', track.title);
|
|
||||||
|
|
||||||
if (!this.ffmpegLoaded) {
|
|
||||||
try {
|
|
||||||
await this.loadFFmpeg();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Metadata] Cannot load FFmpeg, skipping metadata:', error);
|
|
||||||
return audioBlob;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.ffmpeg || !this.fetchFile) {
|
|
||||||
console.error('[Metadata] FFmpeg not properly initialized');
|
|
||||||
return audioBlob;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputName = 'input.flac';
|
|
||||||
const coverName = 'cover.jpg';
|
|
||||||
const outputName = 'output.flac';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
|
||||||
await this.ffmpeg.writeFile(inputName, new Uint8Array(arrayBuffer));
|
|
||||||
console.log('[Metadata] Wrote input file:', inputName, 'size:', arrayBuffer.byteLength);
|
|
||||||
|
|
||||||
let hasCover = false;
|
|
||||||
if (coverImageUrl) {
|
|
||||||
try {
|
|
||||||
console.log('[Metadata] Fetching cover from:', coverImageUrl);
|
|
||||||
const coverData = await this.fetchFile(coverImageUrl);
|
|
||||||
await this.ffmpeg.writeFile(coverName, coverData);
|
|
||||||
hasCover = true;
|
|
||||||
console.log('[Metadata] Cover image written successfully, size:', coverData.length);
|
|
||||||
} catch (coverError) {
|
|
||||||
console.warn('[Metadata] Failed to fetch cover image:', coverError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = this.buildMetadataArgs(track);
|
|
||||||
console.log('[Metadata] Building metadata with', metadata.length / 2, 'fields');
|
|
||||||
|
|
||||||
let args;
|
|
||||||
if (hasCover) {
|
|
||||||
args = [
|
|
||||||
'-i', inputName,
|
|
||||||
'-i', coverName,
|
|
||||||
'-map', '0:a',
|
|
||||||
'-map', '1',
|
|
||||||
'-c:a', 'copy',
|
|
||||||
'-c:v', 'copy',
|
|
||||||
...metadata,
|
|
||||||
'-metadata:s:v', 'title=Album cover',
|
|
||||||
'-metadata:s:v', 'comment=Cover (front)',
|
|
||||||
'-disposition:v', 'attached_pic',
|
|
||||||
outputName
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
args = [
|
|
||||||
'-i', inputName,
|
|
||||||
...metadata,
|
|
||||||
'-c:a', 'copy',
|
|
||||||
outputName
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Metadata] Executing FFmpeg...');
|
|
||||||
|
|
||||||
if (onProgress) {
|
|
||||||
this.ffmpeg.on('progress', ({ progress }) => {
|
|
||||||
onProgress(progress);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ffmpeg.exec(args);
|
|
||||||
console.log('[Metadata] FFmpeg exec completed successfully');
|
|
||||||
|
|
||||||
const outputData = await this.ffmpeg.readFile(outputName);
|
|
||||||
const outputBlob = new Blob([outputData], { type: 'audio/flac' });
|
|
||||||
console.log('[Metadata] ✓ Success! Input:', arrayBuffer.byteLength, 'bytes → Output:', outputBlob.size, 'bytes');
|
|
||||||
|
|
||||||
await this.ffmpeg.deleteFile(inputName);
|
|
||||||
await this.ffmpeg.deleteFile(outputName);
|
|
||||||
if (hasCover) {
|
|
||||||
await this.ffmpeg.deleteFile(coverName);
|
|
||||||
}
|
|
||||||
console.log('[Metadata] Cleanup complete');
|
|
||||||
|
|
||||||
return outputBlob;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Metadata] ✗ Embedding failed:', error);
|
|
||||||
console.error('[Metadata] Error details:', {
|
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
return audioBlob;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildMetadataArgs(track) {
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
if (track.title) {
|
|
||||||
args.push('-metadata', `title=${this.escapeMetadata(track.title)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.artist?.name) {
|
|
||||||
args.push('-metadata', `artist=${this.escapeMetadata(track.artist.name)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.album?.title) {
|
|
||||||
args.push('-metadata', `album=${this.escapeMetadata(track.album.title)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.album?.artist?.name) {
|
|
||||||
args.push('-metadata', `album_artist=${this.escapeMetadata(track.album.artist.name)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.trackNumber) {
|
|
||||||
const trackNum = Number(track.trackNumber);
|
|
||||||
if (Number.isFinite(trackNum) && trackNum > 0) {
|
|
||||||
const totalTracks = track.album?.numberOfTracks;
|
|
||||||
if (totalTracks && Number.isFinite(totalTracks) && totalTracks > 0) {
|
|
||||||
args.push('-metadata', `track=${trackNum}/${totalTracks}`);
|
|
||||||
} else {
|
|
||||||
args.push('-metadata', `track=${trackNum}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.volumeNumber) {
|
|
||||||
const discNum = Number(track.volumeNumber);
|
|
||||||
if (Number.isFinite(discNum) && discNum > 0) {
|
|
||||||
const totalDiscs = track.album?.numberOfVolumes;
|
|
||||||
if (totalDiscs && Number.isFinite(totalDiscs) && totalDiscs > 0) {
|
|
||||||
args.push('-metadata', `disc=${discNum}/${totalDiscs}`);
|
|
||||||
} else {
|
|
||||||
args.push('-metadata', `disc=${discNum}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.album?.releaseDate) {
|
|
||||||
const year = new Date(track.album.releaseDate).getFullYear();
|
|
||||||
if (!isNaN(year)) {
|
|
||||||
args.push('-metadata', `date=${year}`);
|
|
||||||
args.push('-metadata', `year=${year}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.album?.upc) {
|
|
||||||
args.push('-metadata', `barcode=${track.album.upc}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.isrc) {
|
|
||||||
args.push('-metadata', `isrc=${track.isrc}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push('-metadata', 'comment=https://monochrome.tf/');
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
escapeMetadata(value) {
|
|
||||||
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Loading…
Reference in a new issue