mirror of
https://codeberg.org/armin/tcdweb.git
synced 2026-09-01 05:30:45 +02:00
init
This commit is contained in:
commit
c985d235a3
8 changed files with 2666 additions and 0 deletions
318
js/metadata.js
Normal file
318
js/metadata.js
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
const Metadata = {
|
||||
parse(arrayBuffer, fileName) {
|
||||
const dv = new DataView(arrayBuffer);
|
||||
const size = arrayBuffer.byteLength;
|
||||
const tags = [];
|
||||
|
||||
if (size >= 10 && this._str(dv, 0, 3) === 'ID3')
|
||||
this._id3v2(dv, size, tags);
|
||||
|
||||
if (size >= 4 && this._str(dv, 0, 4) === 'fLaC')
|
||||
this._flac(dv, size, tags);
|
||||
|
||||
if (size >= 32 && this._str(dv, 0, 8) === 'APETAGEX')
|
||||
this._ape(dv, size, 0, tags);
|
||||
|
||||
if (size >= 128) {
|
||||
const off = size - 128;
|
||||
if (this._str(dv, off, 3) === 'TAG')
|
||||
this._id3v1(dv, off, tags);
|
||||
}
|
||||
|
||||
return tags;
|
||||
},
|
||||
|
||||
_str(dv, off, len) {
|
||||
const b = new Uint8Array(dv.buffer, off, len);
|
||||
const end = b.indexOf(0);
|
||||
return new TextDecoder('latin1').decode(end < 0 ? b : b.subarray(0, end));
|
||||
},
|
||||
|
||||
_utf16(dv, off, max) {
|
||||
if (max < 2) return '';
|
||||
const bom = dv.getUint16(off, false);
|
||||
const le = bom !== 0xFEFF;
|
||||
let s = '', i = off + 2;
|
||||
while (i + 1 < off + max) {
|
||||
const c = dv.getUint16(i, le);
|
||||
if (c === 0) break;
|
||||
if (c >= 0xD800 && c <= 0xDFFF && i + 3 < off + max) {
|
||||
const c2 = dv.getUint16(i + 2, le);
|
||||
s += String.fromCodePoint(0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00));
|
||||
i += 4;
|
||||
} else {
|
||||
s += String.fromCharCode(c);
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
_latin1(dv, off, max) {
|
||||
const b = new Uint8Array(dv.buffer, off, max);
|
||||
const end = b.indexOf(0);
|
||||
return new TextDecoder('latin1').decode(end < 0 ? b : b.subarray(0, end));
|
||||
},
|
||||
|
||||
_ssint(dv, off) {
|
||||
return (dv.getUint8(off) << 21) | (dv.getUint8(off + 1) << 14) |
|
||||
(dv.getUint8(off + 2) << 7) | dv.getUint8(off + 3);
|
||||
},
|
||||
|
||||
_fid(id) {
|
||||
const m = {
|
||||
TIT2: 'Title', TPE1: 'Artist', TPE2: 'Album Artist', TALB: 'Album',
|
||||
TYER: 'Year', TDRC: 'Year', TDRL: 'Release Time', TRCK: 'Track',
|
||||
TPOS: 'Disc', TCON: 'Genre', COMM: 'Comment', TCOP: 'Copyright',
|
||||
TPUB: 'Publisher', TENC: 'Encoded By', TSSE: 'Encoder',
|
||||
TCOM: 'Composer', TEXT: 'Lyricist', TLAN: 'Language', TBPM: 'BPM',
|
||||
TKEY: 'Initial Key', USLT: 'Lyrics',
|
||||
TT2: 'Title', TP1: 'Artist', TP2: 'Album Artist', TAL: 'Album',
|
||||
TYE: 'Year', TRK: 'Track', TPA: 'Disc', TCO: 'Genre',
|
||||
COM: 'Comment', TCR: 'Copyright', TPB: 'Publisher',
|
||||
TEN: 'Encoded By', TSS: 'Encoder', TCM: 'Composer',
|
||||
};
|
||||
return m[id] || id;
|
||||
},
|
||||
|
||||
_rdFrm(dv, off, id, sz) {
|
||||
const enc = dv.getUint8(off);
|
||||
|
||||
if (id === 'COMM' || id === 'COM') {
|
||||
let p = off + 4;
|
||||
if (enc === 0 || enc === 3) {
|
||||
while (p < off + sz && dv.getUint8(p) !== 0) p++;
|
||||
p++;
|
||||
return enc === 0
|
||||
? this._latin1(dv, p, off + sz - p)
|
||||
: this._str(dv, p, off + sz - p);
|
||||
}
|
||||
while (p + 1 < off + sz) {
|
||||
if (dv.getUint16(p, enc === 2) === 0) { p += 2; break; }
|
||||
p += 2;
|
||||
}
|
||||
return this._utf16(dv, p, off + sz - p);
|
||||
}
|
||||
|
||||
if (id === 'WXXX') {
|
||||
let p = off + 1;
|
||||
if (enc === 0 || enc === 3) {
|
||||
while (p < off + sz && dv.getUint8(p) !== 0) p++;
|
||||
return this._str(dv, p + 1, off + sz - p - 1);
|
||||
}
|
||||
while (p + 1 < off + sz) {
|
||||
if (dv.getUint16(p, enc === 2) === 0) { p += 2; break; }
|
||||
p += 2;
|
||||
}
|
||||
return this._str(dv, p, off + sz - p);
|
||||
}
|
||||
|
||||
if (id.startsWith('T') || (id.length === 3 && id.startsWith('T'))) {
|
||||
if (enc === 0) return this._latin1(dv, off + 1, sz - 1);
|
||||
if (enc === 3) return this._str(dv, off + 1, sz - 1);
|
||||
return this._utf16(dv, off + 1, sz - 1);
|
||||
}
|
||||
|
||||
if (id === 'APIC' || id === 'PIC') return null;
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
_id3v2(dv, size, tags) {
|
||||
try {
|
||||
const ver = dv.getUint8(3);
|
||||
const fl = dv.getUint8(5);
|
||||
const tagSz = this._ssint(dv, 6);
|
||||
let off = 10, end = off + tagSz;
|
||||
if (end > size) return;
|
||||
const vs = `ID3v2.${ver}.${dv.getUint8(4)}`;
|
||||
|
||||
if (fl & 0x40) {
|
||||
off += ver >= 4 ? this._ssint(dv, off) : dv.getUint32(off, false);
|
||||
}
|
||||
|
||||
while (off + (ver === 2 ? 6 : 10) <= end) {
|
||||
const fid = this._str(dv, off, ver === 2 ? 3 : 4);
|
||||
if (fid.length < (ver === 2 ? 3 : 4) || fid.indexOf('\x00') >= 0) break;
|
||||
|
||||
let fsz;
|
||||
if (ver === 2) {
|
||||
fsz = (dv.getUint8(off + 3) << 16) | (dv.getUint8(off + 4) << 8) | dv.getUint8(off + 5);
|
||||
off += 6;
|
||||
} else {
|
||||
fsz = ver >= 4 ? this._ssint(dv, off + 4) : dv.getUint32(off + 4, false);
|
||||
off += 10;
|
||||
}
|
||||
if (fsz === 0 || off + fsz > end) break;
|
||||
|
||||
const val = this._rdFrm(dv, off, fid, fsz);
|
||||
if (val) tags.push({ source: vs, key: this._fid(fid), value: val });
|
||||
off += fsz;
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
_id3v1(dv, off, tags) {
|
||||
try {
|
||||
const src = 'ID3v1';
|
||||
const title = this._str(dv, off + 3, 30).trim();
|
||||
const artist = this._str(dv, off + 33, 30).trim();
|
||||
const album = this._str(dv, off + 63, 30).trim();
|
||||
const year = this._str(dv, off + 93, 4).trim();
|
||||
const comment = this._str(dv, off + 97, 30).trim();
|
||||
const genre = dv.getUint8(off + 127);
|
||||
const genres = [
|
||||
'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop',
|
||||
'Jazz','Metal','New Age','Oldies','Other','Pop','R&B','Rap','Reggae','Rock',
|
||||
'Techno','Industrial','Alternative','Ska','Death Metal','Pranks','Soundtrack',
|
||||
'Euro-Techno','Ambient','Trip-Hop','Vocal','Jazz+Funk','Fusion','Trance',
|
||||
'Classical','Instrumental','Acid','House','Game','Sound Clip','Gospel',
|
||||
'Noise','Alt Rock','Bass','Soul','Punk','Space','Meditative','Instrumental Pop',
|
||||
'Instrumental Rock','Ethnic','Gothic','Darkwave','Techno-Industrial','Electronic',
|
||||
'Pop-Folk','Eurodance','Dream','Southern Rock','Comedy','Cult','Gangsta Rap',
|
||||
'Top 40','Christian Rap','Pop/Funk','Jungle','Native American','Cabaret',
|
||||
'New Wave','Psychedelic','Rave','Showtunes','Trailer','Lo-Fi','Tribal',
|
||||
'Acid Punk','Acid Jazz','Polka','Retro','Musical','Rock & Roll','Hard Rock',
|
||||
'Folk','Folk/Rock','National Folk','Swing','Fast-Fusion','Bebob','Latin',
|
||||
'Revival','Celtic','Bluegrass','Avantgarde','Gothic Rock','Progressive Rock',
|
||||
'Psychedelic Rock','Symphonic Rock','Slow Rock','Big Band','Chorus',
|
||||
'Easy Listening','Acoustic','Humour','Speech','Chanson','Opera','Chamber Music',
|
||||
'Sonata','Symphony','Booty Bass','Primus','Porn Groove','Satire','Slow Jam',
|
||||
'Club','Tango','Samba','Folklore','Ballad','Power Ballad','Rhythmic Soul',
|
||||
'Freestyle','Duet','Punk Rock','Drum Solo','A Cappella','Euro-House','Dance Hall',
|
||||
'Goa','Drum & Bass','Club-House','Hardcore','Terror','Indie','BritPop',
|
||||
'Negerpunk','Polsk Punk','Beat','Christian Gangsta Rap','Heavy Metal','Black Metal',
|
||||
'Crossover','Contemporary Christian','Christian Rock','Merengue','Salsa',
|
||||
'Thrash Metal','Anime','JPop','Synthpop'
|
||||
];
|
||||
if (title) tags.push({ source: src, key: 'Title', value: title });
|
||||
if (artist) tags.push({ source: src, key: 'Artist', value: artist });
|
||||
if (album) tags.push({ source: src, key: 'Album', value: album });
|
||||
if (year) tags.push({ source: src, key: 'Year', value: year });
|
||||
if (comment) tags.push({ source: src, key: 'Comment', value: comment });
|
||||
if (genre >= 0 && genre < genres.length)
|
||||
tags.push({ source: src, key: 'Genre', value: genres[genre] });
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
_flac(dv, size, tags) {
|
||||
try {
|
||||
let off = 4;
|
||||
let last = 0;
|
||||
while (!last && off + 4 <= size) {
|
||||
last = dv.getUint8(off) >> 7;
|
||||
const type = dv.getUint8(off) & 0x7F;
|
||||
const blen = (dv.getUint8(off + 1) << 16) | (dv.getUint8(off + 2) << 8) | dv.getUint8(off + 3);
|
||||
off += 4;
|
||||
if (off + blen > size) break;
|
||||
|
||||
if (type === 0) {
|
||||
const bps = ((dv.getUint16(off + 13) & 0xFF) >> 4) + 1;
|
||||
const sr = (dv.getUint16(off + 10) & 0xFFFF) | ((dv.getUint16(off + 9) & 0x0F) << 16);
|
||||
const ch = ((dv.getUint16(off + 12) & 0x0E) >> 1) + 1;
|
||||
tags.push({ source: 'FLAC', key: 'Sample Rate', value: `${sr} Hz` });
|
||||
tags.push({ source: 'FLAC', key: 'Channels', value: `${ch}` });
|
||||
tags.push({ source: 'FLAC', key: 'Bit Depth', value: `${bps}` });
|
||||
}
|
||||
|
||||
if (type === 4) {
|
||||
const vlen = dv.getUint32(off, true);
|
||||
off += 4 + vlen;
|
||||
const ntags = dv.getUint32(off, true);
|
||||
off += 4;
|
||||
for (let i = 0; i < ntags && off < size; i++) {
|
||||
const tlen = dv.getUint32(off, true);
|
||||
off += 4;
|
||||
if (tlen === 0 || off + tlen > size) break;
|
||||
const raw = this._str(dv, off, tlen);
|
||||
const eq = raw.indexOf('=');
|
||||
if (eq > 0) {
|
||||
const k = raw.substring(0, eq);
|
||||
const v = raw.substring(eq + 1);
|
||||
tags.push({ source: 'FLAC', key: k, value: v });
|
||||
}
|
||||
off += tlen;
|
||||
}
|
||||
} else {
|
||||
off += blen;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
_ape(dv, size, off, tags) {
|
||||
try {
|
||||
const ver = dv.getUint32(off + 8, false);
|
||||
const tagSz = dv.getUint32(off + 12, false);
|
||||
const nItems = dv.getUint32(off + 16, false);
|
||||
const flags = dv.getUint32(off + 20, false);
|
||||
let p = off + (ver >= 2000 ? 32 : 26);
|
||||
|
||||
const src = `APE${ver}`;
|
||||
for (let i = 0; i < nItems && p + 8 <= size; i++) {
|
||||
const valSz = dv.getUint32(p, true);
|
||||
const itemFlags = dv.getUint32(p + 4, false);
|
||||
p += 8;
|
||||
if (valSz === 0 || p + valSz > size) break;
|
||||
|
||||
const keyEnd = p;
|
||||
let kp = keyEnd;
|
||||
while (kp < size && dv.getUint8(kp) !== 0) kp++;
|
||||
const key = this._str(dv, keyEnd, kp - keyEnd);
|
||||
if (!key) break;
|
||||
kp++;
|
||||
|
||||
const val = this._str(dv, kp, valSz);
|
||||
tags.push({ source: src, key, value: val });
|
||||
p = kp + valSz;
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
_mp3Sync(dv, off, size) {
|
||||
while (off + 4 <= size) {
|
||||
if (dv.getUint8(off) === 0xFF && (dv.getUint8(off + 1) & 0xE0) === 0xE0) return off;
|
||||
off++;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
getFormatInfo(arrayBuffer, codecName) {
|
||||
const info = { bitrateMode: null, stereoMode: null };
|
||||
if (codecName !== 'MP3') return info;
|
||||
|
||||
try {
|
||||
const dv = new DataView(arrayBuffer);
|
||||
const size = arrayBuffer.byteLength;
|
||||
|
||||
let off = 0;
|
||||
const tagEnd = this._id3v2Size(dv, size);
|
||||
if (tagEnd > 0) off = tagEnd;
|
||||
|
||||
const sync = this._mp3Sync(dv, off, size);
|
||||
if (sync < 0) return info;
|
||||
|
||||
const h = dv.getUint16(sync + 2, false);
|
||||
const chMode = (h >> 6) & 3;
|
||||
const chNames = ['Stereo', 'Joint Stereo', 'Dual Channel', 'Mono'];
|
||||
info.stereoMode = chNames[chMode] || null;
|
||||
|
||||
const scanEnd = Math.min(sync + 200, size - 4);
|
||||
for (let p = sync + 4; p < scanEnd; p++) {
|
||||
const tag = this._str(dv, p, 4);
|
||||
if (tag === 'Xing' || tag === 'Info') {
|
||||
const flags = dv.getUint32(p + 4, false);
|
||||
info.bitrateMode = (flags & 1) ? 'VBR' : 'CBR';
|
||||
return info;
|
||||
}
|
||||
}
|
||||
info.bitrateMode = 'CBR';
|
||||
} catch (_) {}
|
||||
return info;
|
||||
},
|
||||
|
||||
_id3v2Size(dv, size) {
|
||||
if (size < 10 || this._str(dv, 0, 3) !== 'ID3') return 0;
|
||||
return 10 + this._ssint(dv, 6);
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue