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(); }); });