diff --git a/packages/core/src/imports/parsers.ts b/packages/core/src/imports/parsers.ts index 3c957c3..138b09e 100644 --- a/packages/core/src/imports/parsers.ts +++ b/packages/core/src/imports/parsers.ts @@ -424,6 +424,11 @@ function addTraktDocument(agg: TraktAggregate, json: unknown): void { } } +/** JSON.parse that tolerates a leading UTF-8 BOM (as Blob.json() does). */ +function parseJsonText(text: string): unknown { + return JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text); +} + /** * Parse an uploaded Trakt export: Trakt's official ZIP (watched-history-*.json, * ratings-*.json, lists-watchlist.json, …), any single JSON file from it, or the @@ -447,7 +452,7 @@ export async function parseTraktExport(file: Blob): Promise { const name = entry.entryName.split("/").pop() ?? entry.entryName; if (!name.toLowerCase().endsWith(".json")) continue; try { - addTraktDocument(agg, JSON.parse(entry.getData().toString("utf-8"))); + addTraktDocument(agg, parseJsonText(entry.getData().toString("utf-8"))); } catch { log.debug(`Skipping unreadable Trakt export entry: ${entry.entryName}`); } @@ -455,7 +460,7 @@ export async function parseTraktExport(file: Blob): Promise { } else { let json: unknown; try { - json = JSON.parse(buf.toString("utf8")); + json = parseJsonText(buf.toString("utf8")); } catch { throw new Error("Invalid JSON file"); } diff --git a/packages/core/test/import-parsers.test.ts b/packages/core/test/import-parsers.test.ts index 612e3fa..2f3bbcb 100644 --- a/packages/core/test/import-parsers.test.ts +++ b/packages/core/test/import-parsers.test.ts @@ -1362,6 +1362,18 @@ describe("parseTraktExport", () => { ); }); + test("accepts a JSON file with a UTF-8 byte-order mark", async () => { + const bytes = new Uint8Array([ + 0xef, + 0xbb, + 0xbf, + ...new TextEncoder().encode(JSON.stringify(historyItems)), + ]); + const result = await parseTraktExport(new Blob([bytes], { type: "application/json" })); + expect(result.data.movies).toHaveLength(1); + expect(result.data.episodes).toHaveLength(1); + }); + test("skips an unparsable JSON entry inside the ZIP", async () => { const zip = createZip({ "watched-history-1.json": JSON.stringify(historyItems),