Surface repeated keys are string[] for Vorbis-style metadata (fixes #490)

This commit is contained in:
Vanilagy
2026-09-09 14:01:31 +02:00
parent 533c857a31
commit b2b408749c
3 changed files with 30 additions and 19 deletions
+21 -13
View File
@@ -2597,7 +2597,13 @@ export const readVorbisComments = (bytes: Uint8Array, metadataTags: MetadataTags
const value = string.slice(separatorIndex + 1);
metadataTags.raw ??= {};
metadataTags.raw[key] ??= value;
if (Array.isArray(metadataTags.raw[key])) {
metadataTags.raw[key] = [...metadataTags.raw[key], value];
} else if (typeof metadataTags.raw[key] === 'string') {
metadataTags.raw[key] = [metadataTags.raw[key], value];
} else {
metadataTags.raw[key] ??= value;
}
switch (key) {
case 'TITLE': {
@@ -2728,7 +2734,7 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
commentHeaderParts.push(currentBuffer);
const writtenTags = new Set<string>();
const writtenTags: string[] = [];
const addCommentTag = (key: string, value: string) => {
const joined = `${key}=${value}`;
const encoded = textEncoder.encode(joined);
@@ -2740,7 +2746,7 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
currentBuffer.set(encoded, 4);
commentHeaderParts.push(currentBuffer);
writtenTags.add(key);
writtenTags.push(key);
};
for (const { key, value } of keyValueIterator(tags)) {
@@ -2770,12 +2776,7 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
}; break;
case 'date': {
const rawVersion = tags.raw?.['DATE'] ?? tags.raw?.['date'];
if (rawVersion && typeof rawVersion === 'string') {
addCommentTag('DATE', rawVersion);
} else {
addCommentTag('DATE', value.toISOString().slice(0, 10));
}
addCommentTag('DATE', value.toISOString().slice(0, 10));
}; break;
case 'comment': {
@@ -2803,8 +2804,7 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
}; break;
case 'images': {
// For example, in .flac, we put the pictures in a different section,
// not in the Vorbis comment header.
// For example, in .flac, we put the pictures in a different section, not in the Vorbis comment header.
if (!writeImages) {
break;
}
@@ -2861,18 +2861,26 @@ export const createVorbisComments = (headerBytes: Uint8Array, tags: MetadataTags
if (tags.raw) {
for (const key in tags.raw) {
const value = tags.raw[key] ?? tags.raw[key.toLowerCase()];
if (key === 'vendor' || value == null || writtenTags.has(key)) {
if (key === 'vendor' || value == null || writtenTags.includes(key)) {
continue;
}
if (typeof value === 'string') {
addCommentTag(key, value);
} else if (Array.isArray(value)) {
// String arrays turn into the tag being repeated for each element
const isOnlyStrings = value.every(x => typeof x === 'string');
if (isOnlyStrings) {
for (const elem of value) {
addCommentTag(key, elem);
}
}
}
}
}
const listLengthBuffer = new Uint8Array(4);
toDataView(listLengthBuffer).setUint32(0, writtenTags.size, true);
toDataView(listLengthBuffer).setUint32(0, writtenTags.length, true);
commentHeaderParts.splice(2, 0, listLengthBuffer); // Insert after the header and vendor section
// Merge all comment header parts into a single buffer
+1 -1
View File
@@ -1101,7 +1101,7 @@ export class Conversion {
const inputAndOutputFormatMatch = inputFormat.mimeType === this.output.format.mimeType;
const rawTagsAreUnchanged = inputTags.raw === outputTags.raw;
if (inputTags.raw && rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
if (rawTagsAreUnchanged && !inputAndOutputFormatMatch) {
// If the input and output formats aren't the same, copying over raw metadata tags makes no sense and
// only results in junk tags, so let's cut them out.
delete outputTags.raw;
+8 -5
View File
@@ -76,14 +76,16 @@ export type MetadataTags = {
* user-defined text frames are exposed as a `Record<string, string>`.
* - ADTS: The ID3v2 tags, just like in MP3.
* - Ogg: The key-value string pairs from the Vorbis-style comment header (see RFC 7845, Section 5.2).
* Additionally, the `'vendor'` key refers to the vendor string within this header.
* Additionally, the `'vendor'` key refers to the vendor string within this header. If a key exists more than once,
* a string array is used instead.
* - WAVE: The individual metadata chunks within the RIFF INFO chunk. Values are always ISO 8859-1 strings.
* - FLAC: The key-value string pairs from the vorbis metadata block (see RFC 9639, Section D.2.3).
* Additionally, the `'vendor'` key refers to the vendor string within this header. If ID3v2 tags appear at the
* start of the file, their content is stored just like for MP3.
* Additionally, the `'vendor'` key refers to the vendor string within this header. If a key exists more than once,
* a string array is used instead. If ID3v2 tags appear at the start of the file, their content is stored just like
* for MP3.
* - MPEG-TS: Not supported.
*/
raw?: Record<string, string | Uint8Array | RichImageData | AttachedFile | Record<string, string> | null>;
raw?: Record<string, string | string[] | Uint8Array | RichImageData | AttachedFile | Record<string, string> | null>;
};
/**
@@ -238,13 +240,14 @@ export const validateMetadataTags = (tags: MetadataTags) => {
if (
value !== null
&& typeof value !== 'string'
&& !(Array.isArray(value) && value.every(x => typeof x === 'string'))
&& !(value instanceof Uint8Array)
&& !(value instanceof RichImageData)
&& !(value instanceof AttachedFile)
&& !isRecordStringString(value)
) {
throw new TypeError(
'Each value in tags.raw must be a string, Uint8Array, RichImageData, AttachedFile, '
'Each value in tags.raw must be a string, string array, Uint8Array, RichImageData, AttachedFile, '
+ 'Record<string, string>, or null.',
);
}