fix(imports): tolerate a UTF-8 BOM in uploaded Trakt JSON

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-04 15:40:23 -04:00
1 parent 2767b495e3
commit 0c9ef44ea4
2 files changed
+19 -2

No files matched your search

+7 -2
View File
@@ -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, * 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 * ratings-*.json, lists-watchlist.json, …), any single JSON file from it, or the
@@ -447,7 +452,7 @@ export async function parseTraktExport(file: Blob): Promise<ParseResult> {
const name = entry.entryName.split("/").pop() ?? entry.entryName; const name = entry.entryName.split("/").pop() ?? entry.entryName;
if (!name.toLowerCase().endsWith(".json")) continue; if (!name.toLowerCase().endsWith(".json")) continue;
try { try {
addTraktDocument(agg, JSON.parse(entry.getData().toString("utf-8"))); addTraktDocument(agg, parseJsonText(entry.getData().toString("utf-8")));
} catch { } catch {
log.debug(`Skipping unreadable Trakt export entry: ${entry.entryName}`); log.debug(`Skipping unreadable Trakt export entry: ${entry.entryName}`);
} }
@@ -455,7 +460,7 @@ export async function parseTraktExport(file: Blob): Promise<ParseResult> {
} else { } else {
let json: unknown; let json: unknown;
try { try {
json = JSON.parse(buf.toString("utf8")); json = parseJsonText(buf.toString("utf8"));
} catch { } catch {
throw new Error("Invalid JSON file"); throw new Error("Invalid JSON file");
} }
+12
View File
@@ -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 () => { test("skips an unparsable JSON entry inside the ZIP", async () => {
const zip = createZip({ const zip = createZip({
"watched-history-1.json": JSON.stringify(historyItems), "watched-history-1.json": JSON.stringify(historyItems),