128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { parseId3v1 } from "./id3v1.js";
|
|
|
|
/**
|
|
* Build a minimal ID3v1 tag buffer.
|
|
* Returns a 128-byte buffer starting at offset 0 (the entire buffer IS the tag).
|
|
*/
|
|
function buildId3v1Tag({
|
|
title = "",
|
|
artist = "",
|
|
album = "",
|
|
year = "",
|
|
comment = "",
|
|
track = 0,
|
|
genreIdx = 255,
|
|
}: {
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
year?: string;
|
|
comment?: string;
|
|
track?: number;
|
|
genreIdx?: number;
|
|
}): Uint8Array {
|
|
const buf = new Uint8Array(128);
|
|
buf[0] = 0x54;
|
|
buf[1] = 0x41;
|
|
buf[2] = 0x47; // TAG
|
|
|
|
const enc = new TextEncoder();
|
|
const writeField = (str: string, offset: number, maxLen: number) => {
|
|
const bytes = enc.encode(str);
|
|
const len = Math.min(bytes.length, maxLen);
|
|
buf.set(bytes.subarray(0, len), offset);
|
|
};
|
|
|
|
writeField(title, 3, 30);
|
|
writeField(artist, 33, 30);
|
|
writeField(album, 63, 30);
|
|
writeField(year, 93, 4);
|
|
|
|
if (track > 0) {
|
|
writeField(comment, 97, 28);
|
|
buf[125] = 0x00;
|
|
buf[126] = track;
|
|
} else {
|
|
writeField(comment, 97, 30);
|
|
}
|
|
|
|
buf[127] = genreIdx;
|
|
return buf;
|
|
}
|
|
|
|
describe("id3v1 parser", () => {
|
|
it("parses title, artist, album, year", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({
|
|
title: "My Song",
|
|
artist: "My Artist",
|
|
album: "My Album",
|
|
year: "2023",
|
|
});
|
|
const track = parseId3v1(buf);
|
|
expect(track.title).toBe("My Song");
|
|
expect(track.artist).toBe("My Artist");
|
|
expect(track.album).toBe("My Album");
|
|
expect(track.year).toBe(2023);
|
|
expect(track.format).toBe("id3v1");
|
|
});
|
|
|
|
it("parses genre by index", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ genreIdx: 17 });
|
|
const track = parseId3v1(buf);
|
|
expect(track.genre).toBe("Rock");
|
|
});
|
|
|
|
it("parses track number (ID3v1.1)", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ comment: "Nice track", track: 5 });
|
|
const track = parseId3v1(buf);
|
|
expect(track.trackNumber).toBe(5);
|
|
expect(track.comment).toBe("Nice track");
|
|
});
|
|
|
|
it("parses comment without track number", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ comment: "Full comment here" });
|
|
const track = parseId3v1(buf);
|
|
expect(track.comment).toBe("Full comment here");
|
|
expect(track.trackNumber).toBeUndefined();
|
|
});
|
|
|
|
it("strips trailing nulls and whitespace", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ title: "Song " });
|
|
const track = parseId3v1(buf);
|
|
expect(track.title).toBe("Song");
|
|
});
|
|
|
|
it("returns empty track when buffer too short", { timeout: 5000 }, () => {
|
|
const track = parseId3v1(new Uint8Array(64));
|
|
expect(track).toEqual({});
|
|
});
|
|
|
|
it("returns empty track when TAG magic missing", { timeout: 5000 }, () => {
|
|
const buf = new Uint8Array(128);
|
|
// No TAG magic
|
|
const track = parseId3v1(buf);
|
|
expect(track).toEqual({});
|
|
});
|
|
|
|
it("handles unknown genre index gracefully", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ genreIdx: 200 }); // Beyond known list
|
|
const track = parseId3v1(buf);
|
|
expect(track.genre).toBeUndefined();
|
|
});
|
|
|
|
it("returns rawTags with parsed fields", { timeout: 5000 }, () => {
|
|
const buf = buildId3v1Tag({ title: "Test", artist: "Artist" });
|
|
const track = parseId3v1(buf);
|
|
expect(track.rawTags?.["TIT2"]).toBe("Test");
|
|
expect(track.rawTags?.["TPE1"]).toBe("Artist");
|
|
});
|
|
|
|
it("ignores non-numeric year string", { timeout: 5000 }, () => {
|
|
// year = "abcd" → parseInt returns NaN → year should not be set
|
|
const buf = buildId3v1Tag({ title: "Song", year: "abcd" });
|
|
const track = parseId3v1(buf);
|
|
expect(track.year).toBeUndefined();
|
|
});
|
|
});
|