diff --git a/api/lotus.ts b/api/lotus.ts index ca27bfd..4fa4486 100644 --- a/api/lotus.ts +++ b/api/lotus.ts @@ -1,12 +1,7 @@ -import { default as $romanisedArtists } from "~/third-party/lotus/romanised_artists.json" with { type: "json" }; +import { default as romanisedArtists } from "~/third-party/lotus/romanised_artists.json" with { type: "json" }; import { default as artists } from "~/third-party/lotus/artist.json" with { type: "json" }; import { default as combinedArtists } from "~/third-party/lotus/combined_artists.json" with { type: "json" }; -// overrides -const romanisedArtists: Record = { ...$romanisedArtists }; -romanisedArtists["かめりあ"] = "Camellia"; -romanisedArtists["ななひら"] = "Nanahira"; - function normalise(input: Record): typeof lookup & ((key: string) => string) { const lookup = Object.fromEntries( Object.entries(input).map(([k, v]) => [k.toLowerCase(), v]), diff --git a/lib/format/csv/codec.ts b/lib/format/csv/codec.ts index e9f3112..4c75c83 100644 --- a/lib/format/csv/codec.ts +++ b/lib/format/csv/codec.ts @@ -58,12 +58,15 @@ export function parseTrack( path: string, header?: ColumnIndices, ): Result { - if (line === serializeHeader()) { - return Fail(Errors.csv("invalid_columns", "header row", path)); - } - const fields = split(line); + const headers = getPossibleHeaders(fields); + for (const h of headers) { + if (h.join(",") === line) { + return Fail(Errors.csv("invalid_columns", "header row", path)); + } + } + if (fields.length < 3) { return Fail( Errors.csv( @@ -93,13 +96,24 @@ export function parseTrack( } export function serializeTrack(track: DocumentTrack): string { - return [track.artist, track.album, track.title, track.date].map(escape).join(","); + return [track.artist, track.albumArtist ?? "", track.album, track.title, track.date].map(escape).join(","); } export function serializeHeader(): string { return columnFields.join(","); } +function getPossibleHeaders(segments: string[]): (readonly string[])[] { + if (segments.length === 5) { + return [columnFields]; + } + if (segments.length === 4) { + return [columnFields.filter((f) => f !== "album_artist"), columnFields.filter((f) => f !== "date")]; + } + + return [["artist", "album", "title"]]; +} + export function getColumnIndices(line: string): Result { const fields = split(line); const returns: Record = {}; diff --git a/tests/codec/csv.ts b/tests/codec/csv.ts index 7321efb..993195f 100644 --- a/tests/codec/csv.ts +++ b/tests/codec/csv.ts @@ -27,11 +27,11 @@ Deno.test("CSV Codec: escape() wraps and escapes when necessary", () => { Deno.test("CSV Codec: serializeTrack() correctly formats a row", () => { const track: DocumentTrack = { artist: "A", album: "B", title: "C", date: "D" }; - assertEquals(serializeTrack(track), "A,B,C,D"); + assertEquals(serializeTrack(track), "A,,B,C,D"); }); Deno.test("CSV Codec: serializeHeader() matches expected format", () => { - assertEquals(serializeHeader(), "artist,album,title,date"); + assertEquals(serializeHeader(), "artist,album_artist,album,title,date"); }); Deno.test("CSV Codec: parseTrack() successfully parses a valid line", () => { diff --git a/tests/io/csv.ts b/tests/io/csv.ts index a6762a0..ddd43e8 100644 --- a/tests/io/csv.ts +++ b/tests/io/csv.ts @@ -9,7 +9,7 @@ Deno.test("CSV I/O: saveCsvDocument() and loadCsvDocument() roundtrip", async (t const tracks: DocumentTrack[] = [ { artist: "A", album: "B", title: "C", date: "D" }, - { artist: "E, F", album: "G", title: 'H "I"', date: "J" }, // Contains special chars + { artist: "E, F", album: "G", title: 'H "I"', date: "J" }, ]; await t.step("save creates file", async () => { @@ -45,7 +45,7 @@ Deno.test("CSV I/O: markSkipped() updates line and saves", async (t) => { await t.step("mark skipped returns updated doc", async () => { const markResult = await markSkipped(doc, 1); assert(markResult.ok); - assertEquals(markResult.value.rawLines[1], `${SKIP_PREFIX}A,B,C,D`); + assertEquals(markResult.value.rawLines[1], `${SKIP_PREFIX}A,,B,C,D`); assertEquals(markResult.value.skippedCount, 1); });