yanki 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -194,6 +194,7 @@ type SyncNotesResult = Simplify<Pick<GlobalOptions, 'ankiWeb' | 'dryRun' | 'name
194
194
  deletedMedia: string[];
195
195
  duration: number;
196
196
  fixedDatabase: boolean;
197
+ reuploadedMedia: string[];
197
198
  synced: SyncedNote[];
198
199
  }>;
199
200
  /**
package/dist/lib/index.js CHANGED
@@ -534,8 +534,8 @@ function resolveWithBasePath(filePath, options) {
534
534
  return path.join(cwd, filePath);
535
535
  }
536
536
  function stripBasePath(filePath, basePath) {
537
- const regex = new RegExp(`^${basePath}`, "i");
538
- return filePath.replace(regex, "");
537
+ if (filePath.toLowerCase().startsWith(basePath.toLowerCase())) return filePath.slice(basePath.length);
538
+ return filePath;
539
539
  }
540
540
  function getBaseAndQueryParts(filePath) {
541
541
  const directoryPath = path.dirname(filePath);
@@ -1318,7 +1318,7 @@ async function deleteOrphanedDecks(client, activeNotes, originalNotes, dryRun) {
1318
1318
  while (parts.length > 1) {
1319
1319
  parts.pop();
1320
1320
  const parentDeckName = parts.join("::");
1321
- if (activeNoteDeckNames.some((deckName) => parentDeckName.includes(deckName))) break;
1321
+ if (activeNoteDeckNames.some((deckName) => deckName.startsWith(`${parentDeckName}::`))) break;
1322
1322
  orphanedParentDeckNames.push(parentDeckName);
1323
1323
  }
1324
1324
  }
@@ -1409,21 +1409,49 @@ async function uploadMediaForNote(client, note, dryRun, fileAdapter) {
1409
1409
  }
1410
1410
  return uploadedMedia;
1411
1411
  }
1412
- async function deleteUnusedMedia(client, liveNotes, namespace, dryRun) {
1413
- if (dryRun) return [];
1412
+ async function reconcileMedia(client, liveNotes, namespace, dryRun, fileAdapter) {
1413
+ if (dryRun) return {
1414
+ deleted: [],
1415
+ reuploaded: []
1416
+ };
1414
1417
  const slugifiedNamespace = getSlugifiedNamespace(namespace);
1415
- const activeMediaFilenames = [];
1418
+ const expectedMedia = [];
1416
1419
  for (const note of liveNotes) {
1417
1420
  const mediaPaths = extractMediaFromHtml(`${note.fields.Front}\n${note.fields.Back}\n${note.fields.Extra}`);
1418
- for (const { filename } of mediaPaths) activeMediaFilenames.push(filename);
1421
+ for (const media of mediaPaths) expectedMedia.push(media);
1419
1422
  }
1423
+ const activeMediaFilenames = new Set(expectedMedia.map(({ filename }) => filename));
1420
1424
  const allMediaInNamespace = await client.media.getMediaFilesNames({ pattern: `${slugifiedNamespace}-*` });
1421
1425
  const deletedMediaFilenames = [];
1422
- for (const remoteMediaFilename of allMediaInNamespace) if (!activeMediaFilenames.includes(remoteMediaFilename)) {
1426
+ for (const remoteMediaFilename of allMediaInNamespace) if (!activeMediaFilenames.has(remoteMediaFilename)) {
1423
1427
  await client.media.deleteMediaFile({ filename: remoteMediaFilename });
1424
1428
  deletedMediaFilenames.push(remoteMediaFilename);
1425
1429
  }
1426
- return deletedMediaFilenames;
1430
+ const reuploadedMediaFilenames = [];
1431
+ for (const { filename, originalSrc } of expectedMedia) if (!allMediaInNamespace.includes(filename)) try {
1432
+ if (isUrl(originalSrc)) {
1433
+ await client.media.storeMediaFile({
1434
+ deleteExisting: true,
1435
+ filename,
1436
+ url: originalSrc
1437
+ });
1438
+ reuploadedMediaFilenames.push(filename);
1439
+ } else if (fileAdapter === void 0) console.warn(`Could not re-upload local media file "${filename}": no file adapter provided`);
1440
+ else {
1441
+ await client.media.storeMediaFile({
1442
+ data: uint8ArrayToBase64(await fileAdapter.readFileBuffer(originalSrc)),
1443
+ deleteExisting: true,
1444
+ filename
1445
+ });
1446
+ reuploadedMediaFilenames.push(filename);
1447
+ }
1448
+ } catch (error) {
1449
+ console.warn(`Anki could not re-upload media file: "${filename}"\n${String(error)}`);
1450
+ }
1451
+ return {
1452
+ deleted: deletedMediaFilenames,
1453
+ reuploaded: reuploadedMediaFilenames
1454
+ };
1427
1455
  }
1428
1456
  /**
1429
1457
  * Request permission to access Anki through Anki-Connect.
@@ -1467,7 +1495,7 @@ async function cleanNotes(options) {
1467
1495
  const remoteNotes = await getRemoteNotes(client, sanitizedNamespace);
1468
1496
  await deleteNotes(client, remoteNotes, dryRun);
1469
1497
  const deletedDecks = await deleteOrphanedDecks(client, [], remoteNotes, dryRun);
1470
- const deletedMedia = await deleteUnusedMedia(client, [], sanitizedNamespace, dryRun);
1498
+ const { deleted: deletedMedia } = await reconcileMedia(client, [], sanitizedNamespace, dryRun);
1471
1499
  const isChanged = remoteNotes.length > 0 || deletedDecks.length > 0;
1472
1500
  if (!dryRun && ankiWeb && (isChanged || SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED)) await syncToAnkiWeb(client);
1473
1501
  return {
@@ -2712,6 +2740,7 @@ async function syncNotes(allLocalNotes, options) {
2712
2740
  duration: performance.now() - startTime,
2713
2741
  fixedDatabase: false,
2714
2742
  namespace: sanitizedNamespace,
2743
+ reuploadedMedia: [],
2715
2744
  synced: allLocalNotesCopy.map((note) => ({
2716
2745
  action: "ankiUnreachable",
2717
2746
  note
@@ -2784,7 +2813,7 @@ async function syncNotes(allLocalNotes, options) {
2784
2813
  }
2785
2814
  }
2786
2815
  }
2787
- const deletedMedia = await deleteUnusedMedia(client, liveNotes, sanitizedNamespace, dryRun);
2816
+ const { deleted: deletedMedia, reuploaded: reuploadedMedia } = await reconcileMedia(client, liveNotes, sanitizedNamespace, dryRun, fileAdapter ?? void 0);
2788
2817
  const isChanged = deletedDecks.length > 0 || synced.some((note) => note.action !== "unchanged");
2789
2818
  if (!dryRun && ankiWeb && (isChanged || SYNC_TO_ANKI_WEB_EVEN_IF_UNCHANGED)) await syncToAnkiWeb(client);
2790
2819
  return {
@@ -2795,6 +2824,7 @@ async function syncNotes(allLocalNotes, options) {
2795
2824
  duration: performance.now() - startTime,
2796
2825
  fixedDatabase,
2797
2826
  namespace: sanitizedNamespace,
2827
+ reuploadedMedia,
2798
2828
  synced
2799
2829
  };
2800
2830
  }
@@ -2866,7 +2896,7 @@ async function syncFiles(allLocalFilePaths, options) {
2866
2896
  for (const [index, renamedNote] of renamedLocalNotes.entries()) renamedNote.note = reloadedLocalNotes[index].note;
2867
2897
  }
2868
2898
  }
2869
- const { deletedDecks, deletedMedia, fixedDatabase, synced } = await syncNotes(renamedLocalNotes.map((note) => note.note), {
2899
+ const { deletedDecks, deletedMedia, fixedDatabase, reuploadedMedia, synced } = await syncNotes(renamedLocalNotes.map((note) => note.note), {
2870
2900
  ankiConnectOptions,
2871
2901
  ankiWeb,
2872
2902
  checkDatabase,
@@ -2899,6 +2929,7 @@ async function syncFiles(allLocalFilePaths, options) {
2899
2929
  duration: performance.now() - startTime,
2900
2930
  fixedDatabase,
2901
2931
  namespace,
2932
+ reuploadedMedia,
2902
2933
  synced: syncedAndSorted
2903
2934
  };
2904
2935
  }
@@ -2919,6 +2950,7 @@ function formatSyncFilesResult(result, verbose = false) {
2919
2950
  if (totalRenamed > 0) lines.push("", `Local notes renamed: ${totalRenamed}`);
2920
2951
  if (result.deletedDecks.length > 0) lines.push("", `Decks pruned: ${result.deletedDecks.length}`);
2921
2952
  if (result.deletedMedia.length > 0) lines.push("", `Media assets deleted: ${result.deletedMedia.length}`);
2953
+ if (result.reuploadedMedia.length > 0) lines.push("", `Media assets re-uploaded: ${result.reuploadedMedia.length}`);
2922
2954
  if (!result.dryRun) lines.push("", `Database automatically fixed: ${result.fixedDatabase ? "Yes" : "No"}`);
2923
2955
  lines.push("", result.dryRun ? "Sync Plan Details:" : "Sync Details:");
2924
2956
  for (const { action, filePath, note } of synced) if (filePath === void 0) lines.push(` Note ID ${note.noteId} ${capitalize(action)} (From Anki)`);
@@ -1554,6 +1554,7 @@ type SyncNotesResult = Simplify<Pick<GlobalOptions, 'ankiWeb' | 'dryRun' | 'name
1554
1554
  deletedMedia: string[];
1555
1555
  duration: number;
1556
1556
  fixedDatabase: boolean;
1557
+ reuploadedMedia: string[];
1557
1558
  synced: SyncedNote[];
1558
1559
  }>;
1559
1560
  /**