cover images on local files ⁉️

This commit is contained in:
Samidy 2026-02-20 21:57:17 +03:00
parent 790a3b7f94
commit 61da5c47b0
3 changed files with 88 additions and 3 deletions

View file

@ -62,7 +62,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) {
* @param {File} file * @param {File} file
* @returns {Promise<Object>} Track metadata * @returns {Promise<Object>} Track metadata
*/ */
export async function readTrackMetadata(file) { export async function readTrackMetadata(file, siblings = []) {
const metadata = { const metadata = {
title: file.name.replace(/\.[^/.]+$/, ''), title: file.name.replace(/\.[^/.]+$/, ''),
artists: [], artists: [],
@ -90,6 +90,23 @@ export async function readTrackMetadata(file) {
metadata.artist = metadata.artists[0]; metadata.artist = metadata.artists[0];
} }
if (metadata.album.cover === 'assets/appicon.png' && siblings.length > 0) {
const baseName = file.name.substring(0, file.name.lastIndexOf('.'));
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
const coverFile = siblings.find((f) => {
const fName = f.name;
const lastDot = fName.lastIndexOf('.');
if (lastDot === -1) return false;
const fBase = fName.substring(0, lastDot);
const fExt = fName.substring(lastDot).toLowerCase();
return fBase === baseName && imageExtensions.includes(fExt);
});
if (coverFile) {
metadata.album.cover = URL.createObjectURL(coverFile);
}
}
return metadata; return metadata;
} }
@ -101,6 +118,7 @@ async function readFlacMetadata(file, metadata) {
const blocks = parseFlacBlocks(dataView); const blocks = parseFlacBlocks(dataView);
const vorbisBlock = blocks.find((b) => b.type === 4); const vorbisBlock = blocks.find((b) => b.type === 4);
const pictureBlock = blocks.find((b) => b.type === 6);
const artists = []; const artists = [];
if (vorbisBlock) { if (vorbisBlock) {
@ -133,6 +151,28 @@ async function readFlacMetadata(file, metadata) {
if (artists.length > 0) { if (artists.length > 0) {
metadata.artists = artists.flatMap((a) => a.split(/; |\/|\\/)).map((name) => ({ name: name.trim() })); metadata.artists = artists.flatMap((a) => a.split(/; |\/|\\/)).map((name) => ({ name: name.trim() }));
} }
if (pictureBlock) {
try {
let pos = pictureBlock.offset;
pos += 4;
const mimeLen = dataView.getUint32(pos, false);
pos += 4;
const mime = new TextDecoder().decode(new Uint8Array(arrayBuffer, pos, mimeLen));
pos += mimeLen;
const descLen = dataView.getUint32(pos, false);
pos += 4;
pos += descLen;
pos += 16;
const dataLen = dataView.getUint32(pos, false);
pos += 4;
const pictureData = new Uint8Array(arrayBuffer, pos, dataLen);
const blob = new Blob([pictureData], { type: mime });
metadata.album.cover = URL.createObjectURL(blob);
} catch (e) {
console.warn('Error parsing FLAC picture:', e);
}
}
} }
async function readM4aMetadata(file, metadata) { async function readM4aMetadata(file, metadata) {
@ -194,6 +234,11 @@ async function readM4aMetadata(file, metadata) {
metadata.album.title = new TextDecoder().decode( metadata.album.title = new TextDecoder().decode(
new Uint8Array(view.buffer, contentOffset, contentLen) new Uint8Array(view.buffer, contentOffset, contentLen)
); );
} else if (item.type === 'covr') {
const pictureData = new Uint8Array(view.buffer, contentOffset, contentLen);
const mime = getMimeType(pictureData);
const blob = new Blob([pictureData], { type: mime });
metadata.album.cover = URL.createObjectURL(blob);
} }
} }
} }
@ -253,6 +298,37 @@ async function readMp3Metadata(file, metadata) {
const year = readID3Text(frameData); const year = readID3Text(frameData);
if (year) metadata.album.releaseDate = year; if (year) metadata.album.releaseDate = year;
} }
if (frameId === 'APIC') {
try {
const encoding = frameData.getUint8(0);
let mimeType = '';
let pos = 1;
while (pos < frameData.byteLength && frameData.getUint8(pos) !== 0) {
mimeType += String.fromCharCode(frameData.getUint8(pos));
pos++;
}
pos++;
pos++;
let terminator = encoding === 1 || encoding === 2 ? 2 : 1;
while (pos < frameData.byteLength) {
if (frameData.getUint8(pos) === 0) {
if (terminator === 1) {
pos++;
break;
} else if (pos + 1 < frameData.byteLength && frameData.getUint8(pos + 1) === 0) {
pos += 2;
break;
}
}
pos++;
}
const pictureData = new Uint8Array(buffer, offset + pos, frameSize - pos);
const blob = new Blob([pictureData], { type: mimeType || 'image/jpeg' });
metadata.album.cover = URL.createObjectURL(blob);
} catch (e) {
console.warn('Error parsing APIC:', e);
}
}
offset += frameSize; offset += frameSize;
} }
@ -309,6 +385,12 @@ function readID3Text(view) {
return decoder.decode(buffer).replace(/\0/g, ''); return decoder.decode(buffer).replace(/\0/g, '');
} }
function getMimeType(data) {
if (data.length >= 2 && data[0] === 0xff && data[1] === 0xd8) return 'image/jpeg';
if (data.length >= 8 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) return 'image/png';
return 'image/jpeg';
}
/** /**
* Adds Vorbis comment metadata to FLAC files * Adds Vorbis comment metadata to FLAC files
*/ */

View file

@ -108,6 +108,9 @@ export class MusicAPI {
// Cover/artwork methods // Cover/artwork methods
getCoverUrl(id, size = '320') { getCoverUrl(id, size = '320') {
if (typeof id === 'string' && id.startsWith('blob:')) {
return id;
}
if (typeof id === 'string' && id.startsWith('q:')) { if (typeof id === 'string' && id.startsWith('q:')) {
return this.qobuzAPI.getCoverUrl(id.slice(2), size); return this.qobuzAPI.getCoverUrl(id.slice(2), size);
} }

View file

@ -330,7 +330,7 @@ export class UIRenderer {
const trackTitle = getTrackTitle(track); const trackTitle = getTrackTitle(track);
const isCurrentTrack = this.player?.currentTrack?.id === track.id; const isCurrentTrack = this.player?.currentTrack?.id === track.id;
if (track.isLocal) { if (track.isLocal && (!track.album?.cover || track.album.cover === 'assets/appicon.png')) {
showCover = false; showCover = false;
} }
@ -1443,7 +1443,7 @@ export class UIRenderer {
headerDiv.querySelector('h3').textContent = `Local Files (${window.localFilesCache.length})`; headerDiv.querySelector('h3').textContent = `Local Files (${window.localFilesCache.length})`;
} }
if (listContainer) { if (listContainer) {
this.renderListWithTracks(listContainer, window.localFilesCache, false); this.renderListWithTracks(listContainer, window.localFilesCache, true);
} }
} else { } else {
if (introDiv) introDiv.style.display = 'block'; if (introDiv) introDiv.style.display = 'block';