Parse embedded cover art from ID3v2 APIC frames and FLAC picture blocks

This commit is contained in:
Armin 2026-07-08 17:52:35 +02:00
commit aacb427d1b

View file

@ -21,8 +21,11 @@ footer { display: flex !important; align-items: center; gap: 16px; padding: 10px
footer .brand { margin-left: auto; }
.content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
.pinned { flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); padding: 16px 24px; display: flex; flex-direction: row; gap: 20px; align-items: center; }
.player { display: flex; flex-direction: row; width: 100%; align-items: center; gap: 24px; }
.player { display: flex; flex-direction: row; width: 100%; align-items: center; }
canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
.cover-art { width: 120px; height: 120px; flex-shrink: 0; border-radius: 8px; overflow: hidden; background: #272c42; display: flex; align-items: center; justify-content: center; margin-right: 24px; }
.cover-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cover-art .no-cover { font-size: 32px; color: #6b7089; user-select: none; }
.player-right { display: flex; flex-direction: column; width: 320px; flex-shrink: 0; align-items: center; }
.audio-info { font-size: 11px; color: #6b7089; font-variant-numeric: tabular-nums; min-height: 1.4em; }
.player .track-info { margin-bottom: 4px; }
@ -831,6 +834,23 @@ async function readId3v2(file) {
if (frameSize < 1 || frameSize > 10000000) { pos += 10; continue; }
const fData = tagData.slice(pos + 10, pos + 10 + frameSize);
if (fData.length < 2) { pos += frameSize + 10; continue; }
if (frameId === 'APIC') {
let off = 1;
while (off < fData.length && fData[off] !== 0) off++;
const mime = new TextDecoder('iso-8859-1').decode(fData.slice(1, off));
off++;
off++;
if (fData[0] === 0) {
while (off < fData.length && fData[off] !== 0) off++;
off++;
} else {
while (off + 1 < fData.length && (fData[off] !== 0 || fData[off + 1] !== 0)) off += 2;
off += 2;
}
tags.picture = URL.createObjectURL(new Blob([fData.slice(off)], { type: mime || 'image/jpeg' }));
pos += frameSize + 10;
continue;
}
const enc = fData[0];
const text = decodeText(fData, 1, fData.length - 1, enc);
if (!text) { pos += frameSize + 10; continue; }
@ -875,25 +895,43 @@ async function readVorbisComments(file) {
if (data[0] !== 0x66 || data[1] !== 0x4C || data[2] !== 0x61 || data[3] !== 0x43) return null;
let pos = 4;
let commentData = null;
let pictureData = null;
let pictureMime = '';
while (pos + 4 <= data.length) {
const isLast = data[pos] & 0x80;
const blockType = data[pos] & 0x7f;
const blockLen = (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3];
pos += 4;
if (pos + blockLen > data.length) break;
if (blockType === 4) { commentData = data.slice(pos, pos + blockLen); break; }
if (blockType === 4) { commentData = data.slice(pos, pos + blockLen); }
if (blockType === 6) {
let p = pos + 4;
const mimeLen = (data[p] << 24) | (data[p+1] << 16) | (data[p+2] << 8) | data[p+3];
p += 4;
pictureMime = new TextDecoder('iso-8859-1').decode(data.slice(p, p + mimeLen));
p += mimeLen;
const descLen = (data[p] << 24) | (data[p+1] << 16) | (data[p+2] << 8) | data[p+3];
p += 4 + descLen + 16;
const picLen = (data[p] << 24) | (data[p+1] << 16) | (data[p+2] << 8) | data[p+3];
p += 4;
pictureData = data.slice(p, p + picLen);
}
pos += blockLen;
if (isLast) break;
}
if (!commentData) return null;
if (!commentData && !pictureData) return null;
let off = 0;
if (off + 4 > commentData.length) return null;
const tags = {};
if (pictureData) {
tags.picture = URL.createObjectURL(new Blob([pictureData], { type: pictureMime || 'image/jpeg' }));
}
if (!commentData) return tags;
if (off + 4 > commentData.length) return tags;
const vendorLen = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
off += 4 + vendorLen;
if (off + 4 > commentData.length) return null;
const numTags = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
off += 4;
const tags = {};
for (let i = 0; i < numTags; i++) {
if (off + 4 > commentData.length) break;
const tagLen = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
@ -1054,6 +1092,7 @@ async function processFiles(files, paths) {
composer: tags.composer || '',
urlTag: tags.urlTag || '',
encodedBy: tags.encodedBy || '',
picture: tags.picture || '',
filename: file.name,
url,
file,