diff --git a/js/metadata.js b/js/metadata.js index 3a661ca..beb45ff 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -62,7 +62,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { * @param {File} file * @returns {Promise} Track metadata */ -export async function readTrackMetadata(file) { +export async function readTrackMetadata(file, siblings = []) { const metadata = { title: file.name.replace(/\.[^/.]+$/, ''), artists: [], @@ -90,6 +90,23 @@ export async function readTrackMetadata(file) { 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; } @@ -101,6 +118,7 @@ async function readFlacMetadata(file, metadata) { const blocks = parseFlacBlocks(dataView); const vorbisBlock = blocks.find((b) => b.type === 4); + const pictureBlock = blocks.find((b) => b.type === 6); const artists = []; if (vorbisBlock) { @@ -133,6 +151,28 @@ async function readFlacMetadata(file, metadata) { if (artists.length > 0) { 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) { @@ -194,6 +234,11 @@ async function readM4aMetadata(file, metadata) { metadata.album.title = new TextDecoder().decode( 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); 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; } @@ -309,6 +385,12 @@ function readID3Text(view) { 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 */ diff --git a/js/music-api.js b/js/music-api.js index 51ac906..f5fbf4e 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -108,6 +108,9 @@ export class MusicAPI { // Cover/artwork methods getCoverUrl(id, size = '320') { + if (typeof id === 'string' && id.startsWith('blob:')) { + return id; + } if (typeof id === 'string' && id.startsWith('q:')) { return this.qobuzAPI.getCoverUrl(id.slice(2), size); } diff --git a/js/ui.js b/js/ui.js index 56356eb..d679a8d 100644 --- a/js/ui.js +++ b/js/ui.js @@ -330,7 +330,7 @@ export class UIRenderer { const trackTitle = getTrackTitle(track); 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; } @@ -1443,7 +1443,7 @@ export class UIRenderer { headerDiv.querySelector('h3').textContent = `Local Files (${window.localFilesCache.length})`; } if (listContainer) { - this.renderListWithTracks(listContainer, window.localFilesCache, false); + this.renderListWithTracks(listContainer, window.localFilesCache, true); } } else { if (introDiv) introDiv.style.display = 'block';