@kenzuya/mediabunny 1.26.0 → 1.28.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bundles/{mediabunny.mjs → mediabunny.js} +21963 -21388
- package/dist/bundles/mediabunny.min.js +490 -0
- package/dist/modules/shared/mp3-misc.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-demuxer.d.ts +6 -6
- package/dist/modules/src/adts/adts-demuxer.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-muxer.d.ts +4 -4
- package/dist/modules/src/adts/adts-muxer.d.ts.map +1 -1
- package/dist/modules/src/adts/adts-reader.d.ts +1 -1
- package/dist/modules/src/adts/adts-reader.d.ts.map +1 -1
- package/dist/modules/src/avi/avi-demuxer.d.ts +44 -0
- package/dist/modules/src/avi/avi-demuxer.d.ts.map +1 -0
- package/dist/modules/src/avi/avi-misc.d.ts +88 -0
- package/dist/modules/src/avi/avi-misc.d.ts.map +1 -0
- package/dist/modules/src/avi/avi-muxer.d.ts +45 -0
- package/dist/modules/src/avi/avi-muxer.d.ts.map +1 -0
- package/dist/modules/src/avi/riff-writer.d.ts +26 -0
- package/dist/modules/src/avi/riff-writer.d.ts.map +1 -0
- package/dist/modules/src/codec-data.d.ts +8 -3
- package/dist/modules/src/codec-data.d.ts.map +1 -1
- package/dist/modules/src/codec.d.ts +10 -10
- package/dist/modules/src/codec.d.ts.map +1 -1
- package/dist/modules/src/conversion.d.ts +33 -16
- package/dist/modules/src/conversion.d.ts.map +1 -1
- package/dist/modules/src/custom-coder.d.ts +8 -8
- package/dist/modules/src/custom-coder.d.ts.map +1 -1
- package/dist/modules/src/demuxer.d.ts +3 -3
- package/dist/modules/src/demuxer.d.ts.map +1 -1
- package/dist/modules/src/encode.d.ts +8 -8
- package/dist/modules/src/encode.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-demuxer.d.ts +7 -7
- package/dist/modules/src/flac/flac-demuxer.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-misc.d.ts +3 -3
- package/dist/modules/src/flac/flac-misc.d.ts.map +1 -1
- package/dist/modules/src/flac/flac-muxer.d.ts +5 -5
- package/dist/modules/src/flac/flac-muxer.d.ts.map +1 -1
- package/dist/modules/src/id3.d.ts +3 -3
- package/dist/modules/src/id3.d.ts.map +1 -1
- package/dist/modules/src/index.d.ts +20 -20
- package/dist/modules/src/index.d.ts.map +1 -1
- package/dist/modules/src/input-format.d.ts +22 -0
- package/dist/modules/src/input-format.d.ts.map +1 -1
- package/dist/modules/src/input-track.d.ts +8 -8
- package/dist/modules/src/input-track.d.ts.map +1 -1
- package/dist/modules/src/input.d.ts +12 -12
- package/dist/modules/src/isobmff/isobmff-boxes.d.ts +2 -2
- package/dist/modules/src/isobmff/isobmff-boxes.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-demuxer.d.ts +12 -12
- package/dist/modules/src/isobmff/isobmff-demuxer.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-misc.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-muxer.d.ts +11 -11
- package/dist/modules/src/isobmff/isobmff-muxer.d.ts.map +1 -1
- package/dist/modules/src/isobmff/isobmff-reader.d.ts +2 -2
- package/dist/modules/src/isobmff/isobmff-reader.d.ts.map +1 -1
- package/dist/modules/src/matroska/ebml.d.ts +3 -3
- package/dist/modules/src/matroska/ebml.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-demuxer.d.ts +13 -13
- package/dist/modules/src/matroska/matroska-demuxer.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-input.d.ts +33 -0
- package/dist/modules/src/matroska/matroska-input.d.ts.map +1 -0
- package/dist/modules/src/matroska/matroska-misc.d.ts.map +1 -1
- package/dist/modules/src/matroska/matroska-muxer.d.ts +5 -5
- package/dist/modules/src/matroska/matroska-muxer.d.ts.map +1 -1
- package/dist/modules/src/media-sink.d.ts +5 -5
- package/dist/modules/src/media-sink.d.ts.map +1 -1
- package/dist/modules/src/media-source.d.ts +22 -4
- package/dist/modules/src/media-source.d.ts.map +1 -1
- package/dist/modules/src/metadata.d.ts +2 -2
- package/dist/modules/src/metadata.d.ts.map +1 -1
- package/dist/modules/src/misc.d.ts +5 -4
- package/dist/modules/src/misc.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-demuxer.d.ts +7 -7
- package/dist/modules/src/mp3/mp3-demuxer.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-muxer.d.ts +4 -4
- package/dist/modules/src/mp3/mp3-muxer.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-reader.d.ts +2 -2
- package/dist/modules/src/mp3/mp3-reader.d.ts.map +1 -1
- package/dist/modules/src/mp3/mp3-writer.d.ts +1 -1
- package/dist/modules/src/mp3/mp3-writer.d.ts.map +1 -1
- package/dist/modules/src/muxer.d.ts +4 -4
- package/dist/modules/src/muxer.d.ts.map +1 -1
- package/dist/modules/src/node.d.ts +1 -1
- package/dist/modules/src/ogg/ogg-demuxer.d.ts +7 -7
- package/dist/modules/src/ogg/ogg-demuxer.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-misc.d.ts +1 -1
- package/dist/modules/src/ogg/ogg-misc.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-muxer.d.ts +5 -5
- package/dist/modules/src/ogg/ogg-muxer.d.ts.map +1 -1
- package/dist/modules/src/ogg/ogg-reader.d.ts +1 -1
- package/dist/modules/src/ogg/ogg-reader.d.ts.map +1 -1
- package/dist/modules/src/output-format.d.ts +51 -6
- package/dist/modules/src/output-format.d.ts.map +1 -1
- package/dist/modules/src/output.d.ts +13 -13
- package/dist/modules/src/output.d.ts.map +1 -1
- package/dist/modules/src/packet.d.ts +1 -1
- package/dist/modules/src/packet.d.ts.map +1 -1
- package/dist/modules/src/pcm.d.ts.map +1 -1
- package/dist/modules/src/reader.d.ts +2 -2
- package/dist/modules/src/reader.d.ts.map +1 -1
- package/dist/modules/src/sample.d.ts +57 -15
- package/dist/modules/src/sample.d.ts.map +1 -1
- package/dist/modules/src/source.d.ts +3 -3
- package/dist/modules/src/source.d.ts.map +1 -1
- package/dist/modules/src/subtitles.d.ts +1 -1
- package/dist/modules/src/subtitles.d.ts.map +1 -1
- package/dist/modules/src/target.d.ts +2 -2
- package/dist/modules/src/target.d.ts.map +1 -1
- package/dist/modules/src/tsconfig.tsbuildinfo +1 -1
- package/dist/modules/src/wave/riff-writer.d.ts +1 -1
- package/dist/modules/src/wave/riff-writer.d.ts.map +1 -1
- package/dist/modules/src/wave/wave-demuxer.d.ts +6 -6
- package/dist/modules/src/wave/wave-demuxer.d.ts.map +1 -1
- package/dist/modules/src/wave/wave-muxer.d.ts +4 -4
- package/dist/modules/src/wave/wave-muxer.d.ts.map +1 -1
- package/dist/modules/src/writer.d.ts +1 -1
- package/dist/modules/src/writer.d.ts.map +1 -1
- package/dist/packages/eac3/eac3.wasm +0 -0
- package/dist/packages/eac3/mediabunny-eac3.js +1058 -0
- package/dist/packages/eac3/mediabunny-eac3.min.js +44 -0
- package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.js +694 -0
- package/dist/packages/mp3-encoder/mediabunny-mp3-encoder.min.js +58 -0
- package/dist/packages/mpeg4/mediabunny-mpeg4.js +1198 -0
- package/dist/packages/mpeg4/mediabunny-mpeg4.min.js +44 -0
- package/dist/packages/mpeg4/xvid.wasm +0 -0
- package/package.json +18 -57
- package/dist/bundles/mediabunny.cjs +0 -26140
- package/dist/bundles/mediabunny.min.cjs +0 -147
- package/dist/bundles/mediabunny.min.mjs +0 -146
- package/dist/mediabunny.d.ts +0 -3319
- package/dist/modules/shared/mp3-misc.js +0 -147
- package/dist/modules/src/adts/adts-demuxer.js +0 -239
- package/dist/modules/src/adts/adts-muxer.js +0 -80
- package/dist/modules/src/adts/adts-reader.js +0 -63
- package/dist/modules/src/codec-data.js +0 -1730
- package/dist/modules/src/codec.js +0 -869
- package/dist/modules/src/conversion.js +0 -1459
- package/dist/modules/src/custom-coder.js +0 -117
- package/dist/modules/src/demuxer.js +0 -12
- package/dist/modules/src/encode.js +0 -442
- package/dist/modules/src/flac/flac-demuxer.js +0 -504
- package/dist/modules/src/flac/flac-misc.js +0 -135
- package/dist/modules/src/flac/flac-muxer.js +0 -222
- package/dist/modules/src/id3.js +0 -848
- package/dist/modules/src/index.js +0 -28
- package/dist/modules/src/input-format.js +0 -480
- package/dist/modules/src/input-track.js +0 -372
- package/dist/modules/src/input.js +0 -188
- package/dist/modules/src/isobmff/isobmff-boxes.js +0 -1480
- package/dist/modules/src/isobmff/isobmff-demuxer.js +0 -2618
- package/dist/modules/src/isobmff/isobmff-misc.js +0 -20
- package/dist/modules/src/isobmff/isobmff-muxer.js +0 -966
- package/dist/modules/src/isobmff/isobmff-reader.js +0 -72
- package/dist/modules/src/matroska/ebml.js +0 -653
- package/dist/modules/src/matroska/matroska-demuxer.js +0 -2133
- package/dist/modules/src/matroska/matroska-misc.js +0 -20
- package/dist/modules/src/matroska/matroska-muxer.js +0 -1017
- package/dist/modules/src/media-sink.js +0 -1736
- package/dist/modules/src/media-source.js +0 -1825
- package/dist/modules/src/metadata.js +0 -193
- package/dist/modules/src/misc.js +0 -623
- package/dist/modules/src/mp3/mp3-demuxer.js +0 -285
- package/dist/modules/src/mp3/mp3-muxer.js +0 -123
- package/dist/modules/src/mp3/mp3-reader.js +0 -26
- package/dist/modules/src/mp3/mp3-writer.js +0 -78
- package/dist/modules/src/muxer.js +0 -50
- package/dist/modules/src/node.js +0 -9
- package/dist/modules/src/ogg/ogg-demuxer.js +0 -763
- package/dist/modules/src/ogg/ogg-misc.js +0 -78
- package/dist/modules/src/ogg/ogg-muxer.js +0 -353
- package/dist/modules/src/ogg/ogg-reader.js +0 -65
- package/dist/modules/src/output-format.js +0 -527
- package/dist/modules/src/output.js +0 -300
- package/dist/modules/src/packet.js +0 -182
- package/dist/modules/src/pcm.js +0 -85
- package/dist/modules/src/reader.js +0 -236
- package/dist/modules/src/sample.js +0 -1056
- package/dist/modules/src/source.js +0 -1182
- package/dist/modules/src/subtitles.js +0 -575
- package/dist/modules/src/target.js +0 -140
- package/dist/modules/src/wave/riff-writer.js +0 -30
- package/dist/modules/src/wave/wave-demuxer.js +0 -447
- package/dist/modules/src/wave/wave-muxer.js +0 -318
- package/dist/modules/src/writer.js +0 -370
- package/src/adts/adts-demuxer.ts +0 -331
- package/src/adts/adts-muxer.ts +0 -111
- package/src/adts/adts-reader.ts +0 -85
- package/src/codec-data.ts +0 -2078
- package/src/codec.ts +0 -1092
- package/src/conversion.ts +0 -2112
- package/src/custom-coder.ts +0 -197
- package/src/demuxer.ts +0 -24
- package/src/encode.ts +0 -739
- package/src/flac/flac-demuxer.ts +0 -730
- package/src/flac/flac-misc.ts +0 -164
- package/src/flac/flac-muxer.ts +0 -320
- package/src/id3.ts +0 -925
- package/src/index.ts +0 -221
- package/src/input-format.ts +0 -541
- package/src/input-track.ts +0 -529
- package/src/input.ts +0 -235
- package/src/isobmff/isobmff-boxes.ts +0 -1719
- package/src/isobmff/isobmff-demuxer.ts +0 -3190
- package/src/isobmff/isobmff-misc.ts +0 -29
- package/src/isobmff/isobmff-muxer.ts +0 -1348
- package/src/isobmff/isobmff-reader.ts +0 -91
- package/src/matroska/ebml.ts +0 -730
- package/src/matroska/matroska-demuxer.ts +0 -2481
- package/src/matroska/matroska-misc.ts +0 -29
- package/src/matroska/matroska-muxer.ts +0 -1276
- package/src/media-sink.ts +0 -2179
- package/src/media-source.ts +0 -2243
- package/src/metadata.ts +0 -320
- package/src/misc.ts +0 -798
- package/src/mp3/mp3-demuxer.ts +0 -383
- package/src/mp3/mp3-muxer.ts +0 -166
- package/src/mp3/mp3-reader.ts +0 -34
- package/src/mp3/mp3-writer.ts +0 -120
- package/src/muxer.ts +0 -88
- package/src/node.ts +0 -11
- package/src/ogg/ogg-demuxer.ts +0 -1053
- package/src/ogg/ogg-misc.ts +0 -116
- package/src/ogg/ogg-muxer.ts +0 -497
- package/src/ogg/ogg-reader.ts +0 -93
- package/src/output-format.ts +0 -945
- package/src/output.ts +0 -488
- package/src/packet.ts +0 -263
- package/src/pcm.ts +0 -112
- package/src/reader.ts +0 -323
- package/src/sample.ts +0 -1461
- package/src/source.ts +0 -1688
- package/src/subtitles.ts +0 -711
- package/src/target.ts +0 -204
- package/src/tsconfig.json +0 -16
- package/src/wave/riff-writer.ts +0 -36
- package/src/wave/wave-demuxer.ts +0 -529
- package/src/wave/wave-muxer.ts +0 -371
- package/src/writer.ts +0 -490
package/dist/modules/src/id3.js
DELETED
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2025-present, Vanilagy and contributors
|
|
3
|
-
*
|
|
4
|
-
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
-
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
-
*/
|
|
8
|
-
import { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc.js';
|
|
9
|
-
import { coalesceIndex, textDecoder, textEncoder, isIso88591Compatible, assertNever, keyValueIterator, toDataView, } from './misc.js';
|
|
10
|
-
import { readAscii, readBytes, readU32Be, readU8 } from './reader.js';
|
|
11
|
-
export var Id3V2HeaderFlags;
|
|
12
|
-
(function (Id3V2HeaderFlags) {
|
|
13
|
-
Id3V2HeaderFlags[Id3V2HeaderFlags["Unsynchronisation"] = 128] = "Unsynchronisation";
|
|
14
|
-
Id3V2HeaderFlags[Id3V2HeaderFlags["ExtendedHeader"] = 64] = "ExtendedHeader";
|
|
15
|
-
Id3V2HeaderFlags[Id3V2HeaderFlags["ExperimentalIndicator"] = 32] = "ExperimentalIndicator";
|
|
16
|
-
Id3V2HeaderFlags[Id3V2HeaderFlags["Footer"] = 16] = "Footer";
|
|
17
|
-
})(Id3V2HeaderFlags || (Id3V2HeaderFlags = {}));
|
|
18
|
-
export var Id3V2TextEncoding;
|
|
19
|
-
(function (Id3V2TextEncoding) {
|
|
20
|
-
Id3V2TextEncoding[Id3V2TextEncoding["ISO_8859_1"] = 0] = "ISO_8859_1";
|
|
21
|
-
Id3V2TextEncoding[Id3V2TextEncoding["UTF_16_WITH_BOM"] = 1] = "UTF_16_WITH_BOM";
|
|
22
|
-
Id3V2TextEncoding[Id3V2TextEncoding["UTF_16_BE_NO_BOM"] = 2] = "UTF_16_BE_NO_BOM";
|
|
23
|
-
Id3V2TextEncoding[Id3V2TextEncoding["UTF_8"] = 3] = "UTF_8";
|
|
24
|
-
})(Id3V2TextEncoding || (Id3V2TextEncoding = {}));
|
|
25
|
-
export const ID3_V1_TAG_SIZE = 128;
|
|
26
|
-
export const ID3_V2_HEADER_SIZE = 10;
|
|
27
|
-
export const ID3_V1_GENRES = [
|
|
28
|
-
'Blues', 'Classic rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 'Hip-hop', 'Jazz',
|
|
29
|
-
'Metal', 'New age', 'Oldies', 'Other', 'Pop', 'Rhythm and blues', 'Rap', 'Reggae', 'Rock',
|
|
30
|
-
'Techno', 'Industrial', 'Alternative', 'Ska', 'Death metal', 'Pranks', 'Soundtrack',
|
|
31
|
-
'Euro-techno', 'Ambient', 'Trip-hop', 'Vocal', 'Jazz & funk', 'Fusion', 'Trance', 'Classical',
|
|
32
|
-
'Instrumental', 'Acid', 'House', 'Game', 'Sound clip', 'Gospel', 'Noise', 'Alternative rock',
|
|
33
|
-
'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental pop', 'Instrumental rock',
|
|
34
|
-
'Ethnic', 'Gothic', 'Darkwave', 'Techno-industrial', 'Electronic', 'Pop-folk', 'Eurodance',
|
|
35
|
-
'Dream', 'Southern rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 'Christian rap', 'Pop/funk',
|
|
36
|
-
'Jungle music', 'Native US', 'Cabaret', 'New wave', 'Psychedelic', 'Rave', 'Showtunes',
|
|
37
|
-
'Trailer', 'Lo-fi', 'Tribal', 'Acid punk', 'Acid jazz', 'Polka', 'Retro', 'Musical',
|
|
38
|
-
'Rock \'n\' roll', 'Hard rock', 'Folk', 'Folk rock', 'National folk', 'Swing', 'Fast fusion',
|
|
39
|
-
'Bebop', 'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic rock',
|
|
40
|
-
'Progressive rock', 'Psychedelic rock', 'Symphonic rock', 'Slow rock', 'Big band', 'Chorus',
|
|
41
|
-
'Easy listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', 'Chamber music',
|
|
42
|
-
'Sonata', 'Symphony', 'Booty bass', 'Primus', 'Porn groove', 'Satire', 'Slow jam', 'Club',
|
|
43
|
-
'Tango', 'Samba', 'Folklore', 'Ballad', 'Power ballad', 'Rhythmic Soul', 'Freestyle', 'Duet',
|
|
44
|
-
'Punk rock', 'Drum solo', 'A cappella', 'Euro-house', 'Dance hall', 'Goa music', 'Drum & bass',
|
|
45
|
-
'Club-house', 'Hardcore techno', 'Terror', 'Indie', 'Britpop', 'Negerpunk', 'Polsk punk',
|
|
46
|
-
'Beat', 'Christian gangsta rap', 'Heavy metal', 'Black metal', 'Crossover',
|
|
47
|
-
'Contemporary Christian', 'Christian rock', 'Merengue', 'Salsa', 'Thrash metal', 'Anime',
|
|
48
|
-
'Jpop', 'Synthpop', 'Christmas', 'Art rock', 'Baroque', 'Bhangra', 'Big beat', 'Breakbeat',
|
|
49
|
-
'Chillout', 'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', 'Electroclash', 'Emo',
|
|
50
|
-
'Experimental', 'Garage', 'Global', 'IDM', 'Illbient', 'Industro-Goth', 'Jam Band',
|
|
51
|
-
'Krautrock', 'Leftfield', 'Lounge', 'Math rock', 'New romantic', 'Nu-breakz', 'Post-punk',
|
|
52
|
-
'Post-rock', 'Psytrance', 'Shoegaze', 'Space rock', 'Trop rock', 'World music', 'Neoclassical',
|
|
53
|
-
'Audiobook', 'Audio theatre', 'Neue Deutsche Welle', 'Podcast', 'Indie rock', 'G-Funk',
|
|
54
|
-
'Dubstep', 'Garage rock', 'Psybient',
|
|
55
|
-
];
|
|
56
|
-
export const parseId3V1Tag = (slice, tags) => {
|
|
57
|
-
const startPos = slice.filePos;
|
|
58
|
-
tags.raw ??= {};
|
|
59
|
-
tags.raw['TAG'] ??= readBytes(slice, ID3_V1_TAG_SIZE - 3); // Dump the whole tag into the raw metadata
|
|
60
|
-
slice.filePos = startPos;
|
|
61
|
-
const title = readId3V1String(slice, 30);
|
|
62
|
-
if (title)
|
|
63
|
-
tags.title ??= title;
|
|
64
|
-
const artist = readId3V1String(slice, 30);
|
|
65
|
-
if (artist)
|
|
66
|
-
tags.artist ??= artist;
|
|
67
|
-
const album = readId3V1String(slice, 30);
|
|
68
|
-
if (album)
|
|
69
|
-
tags.album ??= album;
|
|
70
|
-
const yearText = readId3V1String(slice, 4);
|
|
71
|
-
const year = Number.parseInt(yearText, 10);
|
|
72
|
-
if (Number.isInteger(year) && year > 0) {
|
|
73
|
-
tags.date ??= new Date(year, 0, 1);
|
|
74
|
-
}
|
|
75
|
-
const commentBytes = readBytes(slice, 30);
|
|
76
|
-
let comment;
|
|
77
|
-
// Check for the ID3v1.1 track number format:
|
|
78
|
-
// The 29th byte (index 28) is a null terminator, and the 30th byte is the track number.
|
|
79
|
-
if (commentBytes[28] === 0 && commentBytes[29] !== 0) {
|
|
80
|
-
const trackNum = commentBytes[29];
|
|
81
|
-
if (trackNum > 0) {
|
|
82
|
-
tags.trackNumber ??= trackNum;
|
|
83
|
-
}
|
|
84
|
-
slice.skip(-30);
|
|
85
|
-
comment = readId3V1String(slice, 28);
|
|
86
|
-
slice.skip(2);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
slice.skip(-30);
|
|
90
|
-
comment = readId3V1String(slice, 30);
|
|
91
|
-
}
|
|
92
|
-
if (comment)
|
|
93
|
-
tags.comment ??= comment;
|
|
94
|
-
const genreIndex = readU8(slice);
|
|
95
|
-
if (genreIndex < ID3_V1_GENRES.length) {
|
|
96
|
-
tags.genre ??= ID3_V1_GENRES[genreIndex];
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
export const readId3V1String = (slice, length) => {
|
|
100
|
-
const bytes = readBytes(slice, length);
|
|
101
|
-
const endIndex = coalesceIndex(bytes.indexOf(0), bytes.length);
|
|
102
|
-
const relevantBytes = bytes.subarray(0, endIndex);
|
|
103
|
-
// Decode as ISO-8859-1
|
|
104
|
-
let str = '';
|
|
105
|
-
for (let i = 0; i < relevantBytes.length; i++) {
|
|
106
|
-
str += String.fromCharCode(relevantBytes[i]);
|
|
107
|
-
}
|
|
108
|
-
return str.trimEnd(); // String also may be padded with spaces
|
|
109
|
-
};
|
|
110
|
-
export const readId3V2Header = (slice) => {
|
|
111
|
-
const startPos = slice.filePos;
|
|
112
|
-
const tag = readAscii(slice, 3);
|
|
113
|
-
const majorVersion = readU8(slice);
|
|
114
|
-
const revision = readU8(slice);
|
|
115
|
-
const flags = readU8(slice);
|
|
116
|
-
const sizeRaw = readU32Be(slice);
|
|
117
|
-
if (tag !== 'ID3' || majorVersion === 0xff || revision === 0xff || (sizeRaw & 0x80808080) !== 0) {
|
|
118
|
-
slice.filePos = startPos;
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
const size = decodeSynchsafe(sizeRaw);
|
|
122
|
-
return { majorVersion, revision, flags, size };
|
|
123
|
-
};
|
|
124
|
-
export const parseId3V2Tag = (slice, header, tags) => {
|
|
125
|
-
// https://id3.org/id3v2.3.0
|
|
126
|
-
if (![2, 3, 4].includes(header.majorVersion)) {
|
|
127
|
-
console.warn(`Unsupported ID3v2 major version: ${header.majorVersion}`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const bytes = readBytes(slice, header.size);
|
|
131
|
-
const reader = new Id3V2Reader(header, bytes);
|
|
132
|
-
if (header.flags & Id3V2HeaderFlags.Footer) {
|
|
133
|
-
reader.removeFooter();
|
|
134
|
-
}
|
|
135
|
-
if ((header.flags & Id3V2HeaderFlags.Unsynchronisation) && header.majorVersion === 3) {
|
|
136
|
-
reader.ununsynchronizeAll();
|
|
137
|
-
}
|
|
138
|
-
if (header.flags & Id3V2HeaderFlags.ExtendedHeader) {
|
|
139
|
-
const extendedHeaderSize = reader.readU32();
|
|
140
|
-
if (header.majorVersion === 3) {
|
|
141
|
-
reader.pos += extendedHeaderSize; // The extended header size excludes itself
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
reader.pos += extendedHeaderSize - 4; // The extended header size includes itself
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
while (reader.pos <= reader.bytes.length - reader.frameHeaderSize()) {
|
|
148
|
-
const frame = reader.readId3V2Frame();
|
|
149
|
-
if (!frame) {
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
const frameStartPos = reader.pos;
|
|
153
|
-
const frameEndPos = reader.pos + frame.size;
|
|
154
|
-
let frameEncrypted = false;
|
|
155
|
-
let frameCompressed = false;
|
|
156
|
-
let frameUnsynchronized = false;
|
|
157
|
-
if (header.majorVersion === 3) {
|
|
158
|
-
frameEncrypted = !!(frame.flags & (1 << 6));
|
|
159
|
-
frameCompressed = !!(frame.flags & (1 << 7));
|
|
160
|
-
}
|
|
161
|
-
else if (header.majorVersion === 4) {
|
|
162
|
-
frameEncrypted = !!(frame.flags & (1 << 2));
|
|
163
|
-
frameCompressed = !!(frame.flags & (1 << 3));
|
|
164
|
-
frameUnsynchronized = !!(frame.flags & (1 << 1))
|
|
165
|
-
|| !!(header.flags & Id3V2HeaderFlags.Unsynchronisation);
|
|
166
|
-
}
|
|
167
|
-
if (frameEncrypted) {
|
|
168
|
-
console.warn(`Skipping encrypted ID3v2 frame ${frame.id}`);
|
|
169
|
-
reader.pos = frameEndPos;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (frameCompressed) {
|
|
173
|
-
console.warn(`Skipping compressed ID3v2 frame ${frame.id}`); // Maybe someday? Idk
|
|
174
|
-
reader.pos = frameEndPos;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (frameUnsynchronized) {
|
|
178
|
-
reader.ununsynchronizeRegion(reader.pos, frameEndPos);
|
|
179
|
-
}
|
|
180
|
-
tags.raw ??= {};
|
|
181
|
-
if (frame.id[0] === 'T') {
|
|
182
|
-
// It's a text frame, let's decode as text
|
|
183
|
-
tags.raw[frame.id] ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// For the others, let's just get the bytes
|
|
187
|
-
tags.raw[frame.id] ??= reader.readBytes(frame.size);
|
|
188
|
-
}
|
|
189
|
-
reader.pos = frameStartPos;
|
|
190
|
-
switch (frame.id) {
|
|
191
|
-
case 'TIT2':
|
|
192
|
-
case 'TT2':
|
|
193
|
-
{
|
|
194
|
-
tags.title ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
195
|
-
}
|
|
196
|
-
;
|
|
197
|
-
break;
|
|
198
|
-
case 'TIT3':
|
|
199
|
-
case 'TT3':
|
|
200
|
-
{
|
|
201
|
-
tags.description ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
202
|
-
}
|
|
203
|
-
;
|
|
204
|
-
break;
|
|
205
|
-
case 'TPE1':
|
|
206
|
-
case 'TP1':
|
|
207
|
-
{
|
|
208
|
-
tags.artist ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
209
|
-
}
|
|
210
|
-
;
|
|
211
|
-
break;
|
|
212
|
-
case 'TALB':
|
|
213
|
-
case 'TAL':
|
|
214
|
-
{
|
|
215
|
-
tags.album ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
216
|
-
}
|
|
217
|
-
;
|
|
218
|
-
break;
|
|
219
|
-
case 'TPE2':
|
|
220
|
-
case 'TP2':
|
|
221
|
-
{
|
|
222
|
-
tags.albumArtist ??= reader.readId3V2EncodingAndText(frameEndPos);
|
|
223
|
-
}
|
|
224
|
-
;
|
|
225
|
-
break;
|
|
226
|
-
case 'TRCK':
|
|
227
|
-
case 'TRK':
|
|
228
|
-
{
|
|
229
|
-
const trackText = reader.readId3V2EncodingAndText(frameEndPos);
|
|
230
|
-
const parts = trackText.split('/');
|
|
231
|
-
const trackNum = Number.parseInt(parts[0], 10);
|
|
232
|
-
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
|
|
233
|
-
if (Number.isInteger(trackNum) && trackNum > 0) {
|
|
234
|
-
tags.trackNumber ??= trackNum;
|
|
235
|
-
}
|
|
236
|
-
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
|
|
237
|
-
tags.tracksTotal ??= tracksTotal;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
;
|
|
241
|
-
break;
|
|
242
|
-
case 'TPOS':
|
|
243
|
-
case 'TPA':
|
|
244
|
-
{
|
|
245
|
-
const discText = reader.readId3V2EncodingAndText(frameEndPos);
|
|
246
|
-
const parts = discText.split('/');
|
|
247
|
-
const discNum = Number.parseInt(parts[0], 10);
|
|
248
|
-
const discsTotal = parts[1] && Number.parseInt(parts[1], 10);
|
|
249
|
-
if (Number.isInteger(discNum) && discNum > 0) {
|
|
250
|
-
tags.discNumber ??= discNum;
|
|
251
|
-
}
|
|
252
|
-
if (discsTotal && Number.isInteger(discsTotal) && discsTotal > 0) {
|
|
253
|
-
tags.discsTotal ??= discsTotal;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
;
|
|
257
|
-
break;
|
|
258
|
-
case 'TCON':
|
|
259
|
-
case 'TCO':
|
|
260
|
-
{
|
|
261
|
-
const genreText = reader.readId3V2EncodingAndText(frameEndPos);
|
|
262
|
-
let match = /^\((\d+)\)/.exec(genreText);
|
|
263
|
-
if (match) {
|
|
264
|
-
const genreNumber = Number.parseInt(match[1]);
|
|
265
|
-
if (ID3_V1_GENRES[genreNumber] !== undefined) {
|
|
266
|
-
tags.genre ??= ID3_V1_GENRES[genreNumber];
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
match = /^\d+$/.exec(genreText);
|
|
271
|
-
if (match) {
|
|
272
|
-
const genreNumber = Number.parseInt(match[0]);
|
|
273
|
-
if (ID3_V1_GENRES[genreNumber] !== undefined) {
|
|
274
|
-
tags.genre ??= ID3_V1_GENRES[genreNumber];
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
tags.genre ??= genreText;
|
|
279
|
-
}
|
|
280
|
-
;
|
|
281
|
-
break;
|
|
282
|
-
case 'TDRC':
|
|
283
|
-
case 'TDAT':
|
|
284
|
-
{
|
|
285
|
-
const dateText = reader.readId3V2EncodingAndText(frameEndPos);
|
|
286
|
-
const date = new Date(dateText);
|
|
287
|
-
if (!Number.isNaN(date.getTime())) {
|
|
288
|
-
tags.date ??= date;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
;
|
|
292
|
-
break;
|
|
293
|
-
case 'TYER':
|
|
294
|
-
case 'TYE':
|
|
295
|
-
{
|
|
296
|
-
const yearText = reader.readId3V2EncodingAndText(frameEndPos);
|
|
297
|
-
const year = Number.parseInt(yearText, 10);
|
|
298
|
-
if (Number.isInteger(year)) {
|
|
299
|
-
tags.date ??= new Date(year, 0, 1);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
;
|
|
303
|
-
break;
|
|
304
|
-
case 'USLT':
|
|
305
|
-
case 'ULT':
|
|
306
|
-
{
|
|
307
|
-
const encoding = reader.readU8();
|
|
308
|
-
reader.pos += 3; // Skip language
|
|
309
|
-
reader.readId3V2Text(encoding, frameEndPos); // Short content description
|
|
310
|
-
tags.lyrics ??= reader.readId3V2Text(encoding, frameEndPos);
|
|
311
|
-
}
|
|
312
|
-
;
|
|
313
|
-
break;
|
|
314
|
-
case 'COMM':
|
|
315
|
-
case 'COM':
|
|
316
|
-
{
|
|
317
|
-
const encoding = reader.readU8();
|
|
318
|
-
reader.pos += 3; // Skip language
|
|
319
|
-
reader.readId3V2Text(encoding, frameEndPos); // Short content description
|
|
320
|
-
tags.comment ??= reader.readId3V2Text(encoding, frameEndPos);
|
|
321
|
-
}
|
|
322
|
-
;
|
|
323
|
-
break;
|
|
324
|
-
case 'APIC':
|
|
325
|
-
case 'PIC':
|
|
326
|
-
{
|
|
327
|
-
const encoding = reader.readId3V2TextEncoding();
|
|
328
|
-
let mimeType;
|
|
329
|
-
if (header.majorVersion === 2) {
|
|
330
|
-
const imageFormat = reader.readAscii(3);
|
|
331
|
-
mimeType = imageFormat === 'PNG'
|
|
332
|
-
? 'image/png'
|
|
333
|
-
: imageFormat === 'JPG'
|
|
334
|
-
? 'image/jpeg'
|
|
335
|
-
: 'image/*';
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
mimeType = reader.readId3V2Text(encoding, frameEndPos);
|
|
339
|
-
}
|
|
340
|
-
const pictureType = reader.readU8();
|
|
341
|
-
const description = reader.readId3V2Text(encoding, frameEndPos).trimEnd(); // Trim ending spaces
|
|
342
|
-
const imageDataSize = frameEndPos - reader.pos;
|
|
343
|
-
if (imageDataSize >= 0) {
|
|
344
|
-
const imageData = reader.readBytes(imageDataSize);
|
|
345
|
-
if (!tags.images)
|
|
346
|
-
tags.images = [];
|
|
347
|
-
tags.images.push({
|
|
348
|
-
data: imageData,
|
|
349
|
-
mimeType,
|
|
350
|
-
kind: pictureType === 3
|
|
351
|
-
? 'coverFront'
|
|
352
|
-
: pictureType === 4
|
|
353
|
-
? 'coverBack'
|
|
354
|
-
: 'unknown',
|
|
355
|
-
description,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
;
|
|
360
|
-
break;
|
|
361
|
-
default:
|
|
362
|
-
{
|
|
363
|
-
reader.pos += frame.size;
|
|
364
|
-
}
|
|
365
|
-
;
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
reader.pos = frameEndPos;
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
// https://id3.org/id3v2.3.0
|
|
372
|
-
export class Id3V2Reader {
|
|
373
|
-
constructor(header, bytes) {
|
|
374
|
-
this.header = header;
|
|
375
|
-
this.bytes = bytes;
|
|
376
|
-
this.pos = 0;
|
|
377
|
-
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
378
|
-
}
|
|
379
|
-
frameHeaderSize() {
|
|
380
|
-
return this.header.majorVersion === 2 ? 6 : 10;
|
|
381
|
-
}
|
|
382
|
-
ununsynchronizeAll() {
|
|
383
|
-
const newBytes = [];
|
|
384
|
-
for (let i = 0; i < this.bytes.length; i++) {
|
|
385
|
-
const value1 = this.bytes[i];
|
|
386
|
-
newBytes.push(value1);
|
|
387
|
-
if (value1 === 0xff && i !== this.bytes.length - 1) {
|
|
388
|
-
const value2 = this.bytes[i];
|
|
389
|
-
if (value2 === 0x00) {
|
|
390
|
-
i++;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
this.bytes = new Uint8Array(newBytes);
|
|
395
|
-
this.view = new DataView(this.bytes.buffer);
|
|
396
|
-
}
|
|
397
|
-
ununsynchronizeRegion(start, end) {
|
|
398
|
-
const newBytes = [];
|
|
399
|
-
for (let i = start; i < end; i++) {
|
|
400
|
-
const value1 = this.bytes[i];
|
|
401
|
-
newBytes.push(value1);
|
|
402
|
-
if (value1 === 0xff && i !== end - 1) {
|
|
403
|
-
const value2 = this.bytes[i + 1];
|
|
404
|
-
if (value2 === 0x00) {
|
|
405
|
-
i++;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const before = this.bytes.subarray(0, start);
|
|
410
|
-
const after = this.bytes.subarray(end);
|
|
411
|
-
this.bytes = new Uint8Array(before.length + newBytes.length + after.length);
|
|
412
|
-
this.bytes.set(before, 0);
|
|
413
|
-
this.bytes.set(newBytes, before.length);
|
|
414
|
-
this.bytes.set(after, before.length + newBytes.length);
|
|
415
|
-
this.view = new DataView(this.bytes.buffer);
|
|
416
|
-
}
|
|
417
|
-
removeFooter() {
|
|
418
|
-
this.bytes = this.bytes.subarray(0, this.bytes.length - ID3_V2_HEADER_SIZE);
|
|
419
|
-
this.view = new DataView(this.bytes.buffer);
|
|
420
|
-
}
|
|
421
|
-
readBytes(length) {
|
|
422
|
-
const slice = this.bytes.subarray(this.pos, this.pos + length);
|
|
423
|
-
this.pos += length;
|
|
424
|
-
return slice;
|
|
425
|
-
}
|
|
426
|
-
readU8() {
|
|
427
|
-
const value = this.view.getUint8(this.pos);
|
|
428
|
-
this.pos += 1;
|
|
429
|
-
return value;
|
|
430
|
-
}
|
|
431
|
-
readU16() {
|
|
432
|
-
const value = this.view.getUint16(this.pos, false);
|
|
433
|
-
this.pos += 2;
|
|
434
|
-
return value;
|
|
435
|
-
}
|
|
436
|
-
readU24() {
|
|
437
|
-
const high = this.view.getUint16(this.pos, false);
|
|
438
|
-
const low = this.view.getUint8(this.pos + 1);
|
|
439
|
-
this.pos += 3;
|
|
440
|
-
return high * 0x100 + low;
|
|
441
|
-
}
|
|
442
|
-
readU32() {
|
|
443
|
-
const value = this.view.getUint32(this.pos, false);
|
|
444
|
-
this.pos += 4;
|
|
445
|
-
return value;
|
|
446
|
-
}
|
|
447
|
-
readAscii(length) {
|
|
448
|
-
let str = '';
|
|
449
|
-
for (let i = 0; i < length; i++) {
|
|
450
|
-
str += String.fromCharCode(this.view.getUint8(this.pos + i));
|
|
451
|
-
}
|
|
452
|
-
this.pos += length;
|
|
453
|
-
return str;
|
|
454
|
-
}
|
|
455
|
-
readId3V2Frame() {
|
|
456
|
-
if (this.header.majorVersion === 2) {
|
|
457
|
-
const id = this.readAscii(3);
|
|
458
|
-
if (id === '\x00\x00\x00') {
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
const size = this.readU24();
|
|
462
|
-
return { id, size, flags: 0 };
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
const id = this.readAscii(4);
|
|
466
|
-
if (id === '\x00\x00\x00\x00') {
|
|
467
|
-
// We've landed in the padding section
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
const sizeRaw = this.readU32();
|
|
471
|
-
let size = this.header.majorVersion === 4
|
|
472
|
-
? decodeSynchsafe(sizeRaw)
|
|
473
|
-
: sizeRaw;
|
|
474
|
-
const flags = this.readU16();
|
|
475
|
-
const headerEndPos = this.pos;
|
|
476
|
-
// Some files may have incorrectly synchsafed/unsynchsafed sizes. To validate which interpretation is valid,
|
|
477
|
-
// we validate a size by skipping ahead and seeing if we land at a valid frame header (or at the end of the
|
|
478
|
-
// tag.
|
|
479
|
-
const isSizeValid = (size) => {
|
|
480
|
-
const nextPos = this.pos + size;
|
|
481
|
-
if (nextPos > this.bytes.length) {
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
if (nextPos <= this.bytes.length - this.frameHeaderSize()) {
|
|
485
|
-
this.pos += size;
|
|
486
|
-
const nextId = this.readAscii(4);
|
|
487
|
-
if (nextId !== '\x00\x00\x00\x00' && !/[0-9A-Z]{4}/.test(nextId)) {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return true;
|
|
492
|
-
};
|
|
493
|
-
if (!isSizeValid(size)) {
|
|
494
|
-
// Flip the synchsafing, and try if this one makes more sense
|
|
495
|
-
const otherSize = this.header.majorVersion === 4
|
|
496
|
-
? sizeRaw
|
|
497
|
-
: decodeSynchsafe(sizeRaw);
|
|
498
|
-
if (isSizeValid(otherSize)) {
|
|
499
|
-
size = otherSize;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
this.pos = headerEndPos;
|
|
503
|
-
return { id, size, flags };
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
readId3V2TextEncoding() {
|
|
507
|
-
const number = this.readU8();
|
|
508
|
-
if (number > 3) {
|
|
509
|
-
throw new Error(`Unsupported text encoding: ${number}`);
|
|
510
|
-
}
|
|
511
|
-
return number;
|
|
512
|
-
}
|
|
513
|
-
readId3V2Text(encoding, until) {
|
|
514
|
-
const startPos = this.pos;
|
|
515
|
-
const data = this.readBytes(until - this.pos);
|
|
516
|
-
switch (encoding) {
|
|
517
|
-
case Id3V2TextEncoding.ISO_8859_1: {
|
|
518
|
-
let str = '';
|
|
519
|
-
for (let i = 0; i < data.length; i++) {
|
|
520
|
-
const value = data[i];
|
|
521
|
-
if (value === 0) {
|
|
522
|
-
this.pos = startPos + i + 1;
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
str += String.fromCharCode(value);
|
|
526
|
-
}
|
|
527
|
-
return str;
|
|
528
|
-
}
|
|
529
|
-
case Id3V2TextEncoding.UTF_16_WITH_BOM: {
|
|
530
|
-
if (data[0] === 0xff && data[1] === 0xfe) {
|
|
531
|
-
const decoder = new TextDecoder('utf-16le');
|
|
532
|
-
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
|
|
533
|
-
this.pos = startPos + Math.min(endIndex + 2, data.length);
|
|
534
|
-
return decoder.decode(data.subarray(2, endIndex));
|
|
535
|
-
}
|
|
536
|
-
else if (data[0] === 0xfe && data[1] === 0xff) {
|
|
537
|
-
const decoder = new TextDecoder('utf-16be');
|
|
538
|
-
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
|
|
539
|
-
this.pos = startPos + Math.min(endIndex + 2, data.length);
|
|
540
|
-
return decoder.decode(data.subarray(2, endIndex));
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
// Treat it like UTF-8, some files do this
|
|
544
|
-
const endIndex = coalesceIndex(data.findIndex(x => x === 0), data.length);
|
|
545
|
-
this.pos = startPos + Math.min(endIndex + 1, data.length);
|
|
546
|
-
return textDecoder.decode(data.subarray(0, endIndex));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
case Id3V2TextEncoding.UTF_16_BE_NO_BOM: {
|
|
550
|
-
const decoder = new TextDecoder('utf-16be');
|
|
551
|
-
const endIndex = coalesceIndex(data.findIndex((x, i) => x === 0 && data[i + 1] === 0 && i % 2 === 0), data.length);
|
|
552
|
-
this.pos = startPos + Math.min(endIndex + 2, data.length);
|
|
553
|
-
return decoder.decode(data.subarray(0, endIndex));
|
|
554
|
-
}
|
|
555
|
-
case Id3V2TextEncoding.UTF_8: {
|
|
556
|
-
const endIndex = coalesceIndex(data.findIndex(x => x === 0), data.length);
|
|
557
|
-
this.pos = startPos + Math.min(endIndex + 1, data.length);
|
|
558
|
-
return textDecoder.decode(data.subarray(0, endIndex));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
readId3V2EncodingAndText(until) {
|
|
563
|
-
if (this.pos >= until) {
|
|
564
|
-
return '';
|
|
565
|
-
}
|
|
566
|
-
const encoding = this.readId3V2TextEncoding();
|
|
567
|
-
return this.readId3V2Text(encoding, until);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
export class Id3V2Writer {
|
|
571
|
-
constructor(writer) {
|
|
572
|
-
this.helper = new Uint8Array(8);
|
|
573
|
-
this.helperView = toDataView(this.helper);
|
|
574
|
-
this.writer = writer;
|
|
575
|
-
}
|
|
576
|
-
writeId3V2Tag(metadata) {
|
|
577
|
-
const tagStartPos = this.writer.getPos();
|
|
578
|
-
// Write ID3v2.4 header
|
|
579
|
-
this.writeAscii('ID3');
|
|
580
|
-
this.writeU8(0x04); // Version 2.4
|
|
581
|
-
this.writeU8(0x00); // Revision 0
|
|
582
|
-
this.writeU8(0x00); // Flags
|
|
583
|
-
this.writeSynchsafeU32(0); // Size placeholder
|
|
584
|
-
const framesStartPos = this.writer.getPos();
|
|
585
|
-
const writtenTags = new Set();
|
|
586
|
-
// Write all metadata frames
|
|
587
|
-
for (const { key, value } of keyValueIterator(metadata)) {
|
|
588
|
-
switch (key) {
|
|
589
|
-
case 'title':
|
|
590
|
-
{
|
|
591
|
-
this.writeId3V2TextFrame('TIT2', value);
|
|
592
|
-
writtenTags.add('TIT2');
|
|
593
|
-
}
|
|
594
|
-
;
|
|
595
|
-
break;
|
|
596
|
-
case 'description':
|
|
597
|
-
{
|
|
598
|
-
this.writeId3V2TextFrame('TIT3', value);
|
|
599
|
-
writtenTags.add('TIT3');
|
|
600
|
-
}
|
|
601
|
-
;
|
|
602
|
-
break;
|
|
603
|
-
case 'artist':
|
|
604
|
-
{
|
|
605
|
-
this.writeId3V2TextFrame('TPE1', value);
|
|
606
|
-
writtenTags.add('TPE1');
|
|
607
|
-
}
|
|
608
|
-
;
|
|
609
|
-
break;
|
|
610
|
-
case 'album':
|
|
611
|
-
{
|
|
612
|
-
this.writeId3V2TextFrame('TALB', value);
|
|
613
|
-
writtenTags.add('TALB');
|
|
614
|
-
}
|
|
615
|
-
;
|
|
616
|
-
break;
|
|
617
|
-
case 'albumArtist':
|
|
618
|
-
{
|
|
619
|
-
this.writeId3V2TextFrame('TPE2', value);
|
|
620
|
-
writtenTags.add('TPE2');
|
|
621
|
-
}
|
|
622
|
-
;
|
|
623
|
-
break;
|
|
624
|
-
case 'trackNumber':
|
|
625
|
-
{
|
|
626
|
-
const string = metadata.tracksTotal !== undefined
|
|
627
|
-
? `${value}/${metadata.tracksTotal}`
|
|
628
|
-
: value.toString();
|
|
629
|
-
this.writeId3V2TextFrame('TRCK', string);
|
|
630
|
-
writtenTags.add('TRCK');
|
|
631
|
-
}
|
|
632
|
-
;
|
|
633
|
-
break;
|
|
634
|
-
case 'discNumber':
|
|
635
|
-
{
|
|
636
|
-
const string = metadata.discsTotal !== undefined
|
|
637
|
-
? `${value}/${metadata.discsTotal}`
|
|
638
|
-
: value.toString();
|
|
639
|
-
this.writeId3V2TextFrame('TPOS', string);
|
|
640
|
-
writtenTags.add('TPOS');
|
|
641
|
-
}
|
|
642
|
-
;
|
|
643
|
-
break;
|
|
644
|
-
case 'genre':
|
|
645
|
-
{
|
|
646
|
-
this.writeId3V2TextFrame('TCON', value);
|
|
647
|
-
writtenTags.add('TCON');
|
|
648
|
-
}
|
|
649
|
-
;
|
|
650
|
-
break;
|
|
651
|
-
case 'date':
|
|
652
|
-
{
|
|
653
|
-
this.writeId3V2TextFrame('TDRC', value.toISOString().slice(0, 10));
|
|
654
|
-
writtenTags.add('TDRC');
|
|
655
|
-
}
|
|
656
|
-
;
|
|
657
|
-
break;
|
|
658
|
-
case 'lyrics':
|
|
659
|
-
{
|
|
660
|
-
this.writeId3V2LyricsFrame(value);
|
|
661
|
-
writtenTags.add('USLT');
|
|
662
|
-
}
|
|
663
|
-
;
|
|
664
|
-
break;
|
|
665
|
-
case 'comment':
|
|
666
|
-
{
|
|
667
|
-
this.writeId3V2CommentFrame(value);
|
|
668
|
-
writtenTags.add('COMM');
|
|
669
|
-
}
|
|
670
|
-
;
|
|
671
|
-
break;
|
|
672
|
-
case 'images':
|
|
673
|
-
{
|
|
674
|
-
const pictureTypeMap = { coverFront: 0x03, coverBack: 0x04, unknown: 0x00 };
|
|
675
|
-
for (const image of value) {
|
|
676
|
-
const pictureType = pictureTypeMap[image.kind] ?? 0x00;
|
|
677
|
-
const description = image.description ?? '';
|
|
678
|
-
this.writeId3V2ApicFrame(image.mimeType, pictureType, description, image.data);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
;
|
|
682
|
-
break;
|
|
683
|
-
case 'tracksTotal':
|
|
684
|
-
case 'discsTotal':
|
|
685
|
-
{
|
|
686
|
-
// Handled with trackNumber and discNumber respectively
|
|
687
|
-
}
|
|
688
|
-
;
|
|
689
|
-
break;
|
|
690
|
-
case 'raw':
|
|
691
|
-
{
|
|
692
|
-
// Handled later
|
|
693
|
-
}
|
|
694
|
-
;
|
|
695
|
-
break;
|
|
696
|
-
default: {
|
|
697
|
-
assertNever(key);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if (metadata.raw) {
|
|
702
|
-
for (const key in metadata.raw) {
|
|
703
|
-
const value = metadata.raw[key];
|
|
704
|
-
if (value == null || key.length !== 4 || writtenTags.has(key)) {
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
let bytes;
|
|
708
|
-
if (typeof value === 'string') {
|
|
709
|
-
const encoded = textEncoder.encode(value);
|
|
710
|
-
bytes = new Uint8Array(encoded.byteLength + 2);
|
|
711
|
-
bytes[0] = Id3V2TextEncoding.UTF_8;
|
|
712
|
-
bytes.set(encoded, 1);
|
|
713
|
-
// Last byte is the null terminator
|
|
714
|
-
}
|
|
715
|
-
else if (value instanceof Uint8Array) {
|
|
716
|
-
bytes = value;
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
this.writeAscii(key);
|
|
722
|
-
this.writeSynchsafeU32(bytes.byteLength);
|
|
723
|
-
this.writeU16(0x0000);
|
|
724
|
-
this.writer.write(bytes);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
const framesEndPos = this.writer.getPos();
|
|
728
|
-
const framesSize = framesEndPos - framesStartPos;
|
|
729
|
-
// Update the size field in the header (synchsafe)
|
|
730
|
-
this.writer.seek(tagStartPos + 6); // Skip 'ID3' + version + revision + flags
|
|
731
|
-
this.writeSynchsafeU32(framesSize);
|
|
732
|
-
this.writer.seek(framesEndPos);
|
|
733
|
-
return framesSize + 10; // +10 for the header size
|
|
734
|
-
}
|
|
735
|
-
writeU8(value) {
|
|
736
|
-
this.helper[0] = value;
|
|
737
|
-
this.writer.write(this.helper.subarray(0, 1));
|
|
738
|
-
}
|
|
739
|
-
writeU16(value) {
|
|
740
|
-
this.helperView.setUint16(0, value, false);
|
|
741
|
-
this.writer.write(this.helper.subarray(0, 2));
|
|
742
|
-
}
|
|
743
|
-
writeU32(value) {
|
|
744
|
-
this.helperView.setUint32(0, value, false);
|
|
745
|
-
this.writer.write(this.helper.subarray(0, 4));
|
|
746
|
-
}
|
|
747
|
-
writeAscii(text) {
|
|
748
|
-
for (let i = 0; i < text.length; i++) {
|
|
749
|
-
this.helper[i] = text.charCodeAt(i);
|
|
750
|
-
}
|
|
751
|
-
this.writer.write(this.helper.subarray(0, text.length));
|
|
752
|
-
}
|
|
753
|
-
writeSynchsafeU32(value) {
|
|
754
|
-
this.writeU32(encodeSynchsafe(value));
|
|
755
|
-
}
|
|
756
|
-
writeIsoString(text) {
|
|
757
|
-
const bytes = new Uint8Array(text.length + 1);
|
|
758
|
-
for (let i = 0; i < text.length; i++) {
|
|
759
|
-
bytes[i] = text.charCodeAt(i);
|
|
760
|
-
}
|
|
761
|
-
bytes[text.length] = 0x00;
|
|
762
|
-
this.writer.write(bytes);
|
|
763
|
-
}
|
|
764
|
-
writeUtf8String(text) {
|
|
765
|
-
const utf8Data = textEncoder.encode(text);
|
|
766
|
-
this.writer.write(utf8Data);
|
|
767
|
-
this.writeU8(0x00);
|
|
768
|
-
}
|
|
769
|
-
writeId3V2TextFrame(frameId, text) {
|
|
770
|
-
const useIso88591 = isIso88591Compatible(text);
|
|
771
|
-
const textDataLength = useIso88591 ? text.length : textEncoder.encode(text).byteLength;
|
|
772
|
-
const frameSize = 1 + textDataLength + 1;
|
|
773
|
-
this.writeAscii(frameId);
|
|
774
|
-
this.writeSynchsafeU32(frameSize);
|
|
775
|
-
this.writeU16(0x0000);
|
|
776
|
-
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
|
|
777
|
-
if (useIso88591) {
|
|
778
|
-
this.writeIsoString(text);
|
|
779
|
-
}
|
|
780
|
-
else {
|
|
781
|
-
this.writeUtf8String(text);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
writeId3V2LyricsFrame(lyrics) {
|
|
785
|
-
const useIso88591 = isIso88591Compatible(lyrics);
|
|
786
|
-
const shortDescription = '';
|
|
787
|
-
const frameSize = 1 + 3 + shortDescription.length + 1 + lyrics.length + 1;
|
|
788
|
-
this.writeAscii('USLT');
|
|
789
|
-
this.writeSynchsafeU32(frameSize);
|
|
790
|
-
this.writeU16(0x0000);
|
|
791
|
-
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
|
|
792
|
-
this.writeAscii('und');
|
|
793
|
-
if (useIso88591) {
|
|
794
|
-
this.writeIsoString(shortDescription);
|
|
795
|
-
this.writeIsoString(lyrics);
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
this.writeUtf8String(shortDescription);
|
|
799
|
-
this.writeUtf8String(lyrics);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
writeId3V2CommentFrame(comment) {
|
|
803
|
-
const useIso88591 = isIso88591Compatible(comment);
|
|
804
|
-
const textDataLength = useIso88591 ? comment.length : textEncoder.encode(comment).byteLength;
|
|
805
|
-
const shortDescription = '';
|
|
806
|
-
const frameSize = 1 + 3 + shortDescription.length + 1 + textDataLength + 1;
|
|
807
|
-
this.writeAscii('COMM');
|
|
808
|
-
this.writeSynchsafeU32(frameSize);
|
|
809
|
-
this.writeU16(0x0000);
|
|
810
|
-
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
|
|
811
|
-
this.writeU8(0x75); // 'u'
|
|
812
|
-
this.writeU8(0x6E); // 'n'
|
|
813
|
-
this.writeU8(0x64); // 'd'
|
|
814
|
-
if (useIso88591) {
|
|
815
|
-
this.writeIsoString(shortDescription);
|
|
816
|
-
this.writeIsoString(comment);
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
this.writeUtf8String(shortDescription);
|
|
820
|
-
this.writeUtf8String(comment);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
writeId3V2ApicFrame(mimeType, pictureType, description, imageData) {
|
|
824
|
-
const useIso88591 = isIso88591Compatible(mimeType) && isIso88591Compatible(description);
|
|
825
|
-
const descriptionDataLength = useIso88591
|
|
826
|
-
? description.length
|
|
827
|
-
: textEncoder.encode(description).byteLength;
|
|
828
|
-
const frameSize = 1 + mimeType.length + 1 + 1 + descriptionDataLength + 1 + imageData.byteLength;
|
|
829
|
-
this.writeAscii('APIC');
|
|
830
|
-
this.writeSynchsafeU32(frameSize);
|
|
831
|
-
this.writeU16(0x0000);
|
|
832
|
-
this.writeU8(useIso88591 ? Id3V2TextEncoding.ISO_8859_1 : Id3V2TextEncoding.UTF_8);
|
|
833
|
-
if (useIso88591) {
|
|
834
|
-
this.writeIsoString(mimeType);
|
|
835
|
-
}
|
|
836
|
-
else {
|
|
837
|
-
this.writeUtf8String(mimeType);
|
|
838
|
-
}
|
|
839
|
-
this.writeU8(pictureType);
|
|
840
|
-
if (useIso88591) {
|
|
841
|
-
this.writeIsoString(description);
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
this.writeUtf8String(description);
|
|
845
|
-
}
|
|
846
|
-
this.writer.write(imageData);
|
|
847
|
-
}
|
|
848
|
-
}
|