mirror of
https://codeberg.org/armin/noiz.git
synced 2026-09-01 14:00:54 +02:00
Parse embedded cover art from ID3v2 APIC frames and FLAC picture blocks
This commit is contained in:
parent
a46be8ed0a
commit
aacb427d1b
1 changed files with 44 additions and 5 deletions
49
index.html
49
index.html
|
|
@ -21,8 +21,11 @@ footer { display: flex !important; align-items: center; gap: 16px; padding: 10px
|
||||||
footer .brand { margin-left: auto; }
|
footer .brand { margin-left: auto; }
|
||||||
.content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
.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; }
|
.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; }
|
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; }
|
.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; }
|
.audio-info { font-size: 11px; color: #6b7089; font-variant-numeric: tabular-nums; min-height: 1.4em; }
|
||||||
.player .track-info { margin-bottom: 4px; }
|
.player .track-info { margin-bottom: 4px; }
|
||||||
|
|
@ -831,6 +834,23 @@ async function readId3v2(file) {
|
||||||
if (frameSize < 1 || frameSize > 10000000) { pos += 10; continue; }
|
if (frameSize < 1 || frameSize > 10000000) { pos += 10; continue; }
|
||||||
const fData = tagData.slice(pos + 10, pos + 10 + frameSize);
|
const fData = tagData.slice(pos + 10, pos + 10 + frameSize);
|
||||||
if (fData.length < 2) { pos += frameSize + 10; continue; }
|
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 enc = fData[0];
|
||||||
const text = decodeText(fData, 1, fData.length - 1, enc);
|
const text = decodeText(fData, 1, fData.length - 1, enc);
|
||||||
if (!text) { pos += frameSize + 10; continue; }
|
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;
|
if (data[0] !== 0x66 || data[1] !== 0x4C || data[2] !== 0x61 || data[3] !== 0x43) return null;
|
||||||
let pos = 4;
|
let pos = 4;
|
||||||
let commentData = null;
|
let commentData = null;
|
||||||
|
let pictureData = null;
|
||||||
|
let pictureMime = '';
|
||||||
while (pos + 4 <= data.length) {
|
while (pos + 4 <= data.length) {
|
||||||
const isLast = data[pos] & 0x80;
|
const isLast = data[pos] & 0x80;
|
||||||
const blockType = data[pos] & 0x7f;
|
const blockType = data[pos] & 0x7f;
|
||||||
const blockLen = (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3];
|
const blockLen = (data[pos + 1] << 16) | (data[pos + 2] << 8) | data[pos + 3];
|
||||||
pos += 4;
|
pos += 4;
|
||||||
if (pos + blockLen > data.length) break;
|
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;
|
pos += blockLen;
|
||||||
if (isLast) break;
|
if (isLast) break;
|
||||||
}
|
}
|
||||||
if (!commentData) return null;
|
if (!commentData && !pictureData) return null;
|
||||||
let off = 0;
|
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);
|
const vendorLen = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
|
||||||
off += 4 + vendorLen;
|
off += 4 + vendorLen;
|
||||||
if (off + 4 > commentData.length) return null;
|
if (off + 4 > commentData.length) return null;
|
||||||
const numTags = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
|
const numTags = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
|
||||||
off += 4;
|
off += 4;
|
||||||
const tags = {};
|
|
||||||
for (let i = 0; i < numTags; i++) {
|
for (let i = 0; i < numTags; i++) {
|
||||||
if (off + 4 > commentData.length) break;
|
if (off + 4 > commentData.length) break;
|
||||||
const tagLen = commentData[off] | (commentData[off + 1] << 8) | (commentData[off + 2] << 16) | (commentData[off + 3] << 24);
|
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 || '',
|
composer: tags.composer || '',
|
||||||
urlTag: tags.urlTag || '',
|
urlTag: tags.urlTag || '',
|
||||||
encodedBy: tags.encodedBy || '',
|
encodedBy: tags.encodedBy || '',
|
||||||
|
picture: tags.picture || '',
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
url,
|
url,
|
||||||
file,
|
file,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue