From 631bd5d4bfddaa0e54429727fa33a68522759dd8 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 26 Oct 2026 09:35:39 +0000 Subject: [PATCH] quak helper fix-missing-thumbnails --file on an empty cache stops fetching collections once it finds every file (closes #223) `helper fix-missing-thumbnails --file` used to wait for a refresh, which on an empty cache fetched the files of every collection. It now finds the named files through the new `freshFilesByID` in `src/cli-read.ts`, by the rule `freshFile` follows. On an empty cache it reads the collection list, then one collection's files at a time, and stores none of the collections or file records it reads. It searches the most recently updated collection first. So a file in several albums is still named after the first of them that `Library.albums.list()` lists. `fixMissingThumbnails` now takes the records the command found, and fetches each original through `lib.originalOf()`. Model: opus-5-5 --- README.md | 20 +++--- TODO.md | 9 +++ src/cli-commands.ts | 27 ++++++-- src/cli-read.ts | 58 ++++++++++++++++ src/thumbnails.ts | 47 +++---------- test/cli/commands.test.ts | 107 +++++++++++++++++++++++++++-- test/cli/log.test.ts | 6 +- test/cli/read.test.ts | 70 +++++++++++++++++++ test/thumbnails/thumbnails.test.ts | 48 ++++++++++--- 9 files changed, 322 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 97d8e56..926ba26 100644 --- a/README.md +++ b/README.md @@ -689,14 +689,15 @@ request, and neither fetches any collection's files nor changes the cache. `files` with a collection ID asks for the same list, then for that one collection's files, and does not change the cache either. On an empty cache, `get` and `get-thumb` ask for the same list, then for one collection's files at -a time, until they find the file. They store none of the collections or file -records they read. The original or thumbnail they fetch is kept in the cache. -Once the cache holds the account's collections, they wait for a refresh instead. -The others wait for a refresh, and so does `files` without a collection ID. With -an empty cache, as on a first run, that refresh fetches the files of every -collection, one collection after another, and stores nothing until it has them -all, which on a large account takes a while. Meanwhile the command logs a line -on stderr for each collection whose files it has fetched, such as +a time, until they find the file. `helper fix-missing-thumbnails --file` does +the same until it finds every file it names. They store none of the collections +or file records they read. The original or thumbnail they fetch is kept in the +cache. Once the cache holds the account's collections, they wait for a refresh +instead. The others wait for a refresh, and so does `files` without a collection +ID. With an empty cache, as on a first run, that refresh fetches the files of +every collection, one collection after another, and stores nothing until it has +them all, which on a large account takes a while. Meanwhile the command logs a +line on stderr for each collection whose files it has fetched, such as `Fetched the files of 3 of 120 collections`, with `--json` too; `backup` does the same while the refresh it starts with runs. stdout holds only the command's output. If that round-trip fails, the command prints the error on one line and @@ -1192,7 +1193,8 @@ It answers with every live file in that collection that decrypts, in the server's order. `collection` is a record that `freshCollections()` returned. The method fetches no other collection's files and stores nothing. `quak files` with a collection ID uses it, and so do `quak get` and `quak get-thumb` on an empty -cache. +cache. `quak helper fix-missing-thumbnails --file` uses it on an empty cache +too. An album or file in the server's answer that quak cannot decrypt, or whose metadata is not JSON, does not fail the refresh. The refresh leaves it out, an diff --git a/TODO.md b/TODO.md index 197ab3d..854ff37 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,15 @@ declares one. # Completed Steps +- 2026-10-26: On an empty cache, `quak helper fix-missing-thumbnails --file` + stops fetching collections' files once it finds every file it names + (https://git.eeqj.de/sneak/quak/issues/223). It used to wait for a refresh, + which on an empty cache fetched the files of every collection. Now it reads + the collection list, then one collection's files at a time, the most recently + updated first. It stores none of the collections or file records it reads. It + fetches each original through `lib.originalOf()`, which keeps it in the cache. + Once the cache holds the account's collections, it still waits for a refresh. + - 2026-10-26: On an empty cache, `quak get` and `quak get-thumb` stop fetching collections' files once they find the file (https://git.eeqj.de/sneak/quak/issues/222). They used to wait for a refresh, diff --git a/src/cli-commands.ts b/src/cli-commands.ts index 1bb7d13..6089b9b 100644 --- a/src/cli-commands.ts +++ b/src/cli-commands.ts @@ -37,9 +37,16 @@ import { thumbnailName, } from "./cli-output.js"; import { type Log, logRefresh } from "./cli-log.js"; -import { freshCollections, freshFiles, freshFile } from "./cli-read.js"; +import { + cachedFilesByID, + freshCollections, + freshFiles, + freshFile, + freshFilesByID, +} from "./cli-read.js"; import { withExtension } from "./filename.js"; import { runMetadataBackup } from "./metadata-backup.js"; +import type { Collection, EnteFile } from "./model/types.js"; import { listMissingThumbnails, fixMissingThumbnails } from "./thumbnails.js"; export interface CliContext { @@ -133,8 +140,9 @@ const readLibraryClient = (client: Client): LibraryClient => ({ // not start downloading the whole account. No refresh starts as the library // opens or on a timer. Each command asks for the refresh it needs. // `collections` and `files` with a collection ID need none (issues #210 and -// #217). Nor do `get` and `get-thumb` on an empty cache (issue #222). While a -// refresh runs, each collection whose files it has fetched is logged, so a +// #217). Nor do `get` and `get-thumb` on an empty cache (issue #222). Nor does +// `helper fix-missing-thumbnails --file` on an empty cache (issue #223). While +// a refresh runs, each collection whose files it has fetched is logged, so a // large account shows progress rather than nothing. const openReadLibrary = (ctx: CliContext, client: Client): Promise => Library.open({ @@ -565,10 +573,13 @@ export const fixMissingThumbnailsCommand = async ( } const lib = await openReadLibrary(ctx, client); try { - // Refresh first so files added since the cache was written are found; - // a failed refresh throws. - await lib.fresh(); - if (fileIDs.length === 0) { + let found: Map; + if (fileIDs.length > 0) { + found = await freshFilesByID(lib, fileIDs); + } else { + // Refresh first so files added since the cache was written are + // checked; a failed refresh throws. + await lib.fresh(); ctx.log("info", "Scanning for missing thumbnails..."); const missing = await listMissingThumbnails( lib, @@ -581,12 +592,14 @@ export const fixMissingThumbnailsCommand = async ( return 0; } ctx.log("info", `Found ${fileIDs.length} file(s) to fix.`); + found = cachedFilesByID(lib, fileIDs); } const results = await fixMissingThumbnails( lib, client, fileIDs, + found, (msg, level) => ctx.log(level ?? "info", msg), ); diff --git a/src/cli-read.ts b/src/cli-read.ts index e5234fa..55b2d99 100644 --- a/src/cli-read.ts +++ b/src/cli-read.ts @@ -12,6 +12,8 @@ // list too, then that one collection's files through `Library.freshFiles()` // (issue #217). On an empty cache, `get` and `get-thumb` read that list, then // one collection's files at a time, until they find the file (issue #222). +// `helper fix-missing-thumbnails --file` does the same until it finds every +// file it names (issue #223). // // `collections` and `files` with a collection ID list in the server's order. // `files` without one lists in the library's own enumeration order @@ -90,3 +92,59 @@ export const freshFile = async ( } return undefined; }; + +// The most recently updated collection first, and the higher ID first on a +// tie. `Library.albums.list()` lists albums in this order. +const byNewestCollection = (a: Collection, b: Collection): number => + b.updationTime - a.updationTime || b.id - a.id; + +// Each file named by `fileIDs` that the cache holds, mapped to its record and +// the collection it was found in. Collections are searched in +// `byNewestCollection` order, and a file is found in the first that holds it. +// So `helper fix-missing-thumbnails` names a file after the same album as +// `helper list-missing-thumbnails`. It reads the cache alone, so the caller +// refreshes first. +export const cachedFilesByID = ( + lib: FreshReadLibrary, + fileIDs: number[], +): Map => { + const wanted = new Set(fileIDs); + const found = new Map(); + const collections = [...lib.listCollections()].sort(byNewestCollection); + for (const collection of collections) { + for (const file of lib.listFiles(collection.id)) { + if (wanted.has(file.id) && !found.has(file.id)) { + found.set(file.id, { file, collection }); + } + } + } + return found; +}; + +// Each file named by `fileIDs` as the server holds it now, mapped to its record +// and collection as `cachedFilesByID` maps it. When the cache holds the +// account's collections, it refreshes and looks the files up in the cache. On +// an empty cache, it reads the collection list, then one collection's files at +// a time, and stops once it has found every file. That search stores nothing. +export const freshFilesByID = async ( + lib: FreshReadLibrary, + fileIDs: number[], +): Promise> => { + // Only a refresh stores collections, so a cache holding any holds them all. + if (lib.listCollections().length > 0) { + await lib.fresh(); + return cachedFilesByID(lib, fileIDs); + } + const wanted = new Set(fileIDs); + const found = new Map(); + const collections = await lib.freshCollections(); + for (const collection of [...collections].sort(byNewestCollection)) { + if (found.size === wanted.size) break; + for (const file of await lib.freshFiles(collection)) { + if (wanted.has(file.id) && !found.has(file.id)) { + found.set(file.id, { file, collection }); + } + } + } + return found; +}; diff --git a/src/thumbnails.ts b/src/thumbnails.ts index 0205296..471866b 100644 --- a/src/thumbnails.ts +++ b/src/thumbnails.ts @@ -5,7 +5,7 @@ import type { Client } from "./client.js"; import type { Library } from "./library/index.js"; import { ApiError } from "./api/client.js"; import { encryptBlob, toBase64 } from "./crypto/index.js"; -import type { EnteFile } from "./model/types.js"; +import type { Collection, EnteFile } from "./model/types.js"; // The server refuses a thumbnail larger than the one it already records for the // file (`thumbnail.size`, the encrypted size), so these encodings are tried @@ -226,16 +226,18 @@ const encryptThumbnailWithin = ( return undefined; }; -// Regenerate and upload a thumbnail for each requested file. Originals are read -// through the library's content cache (`photo.original()`); the generated -// thumbnail is JPEG-encoded, encrypted under the file's own key, and registered -// with the server — the encrypt-and-upload path is unchanged. Each file is +// Regenerate and upload a thumbnail for each requested file. `found` holds the +// record of each requested file that a collection holds, and the collection it +// was found in. Each original is read through the library's content cache, by +// `lib.originalOf(file)`. The new thumbnail is a JPEG, encrypted under the +// file's own key. It is uploaded, then registered with the server. Each file is // resolved to one outcome (fixed / skipped / failed) and a failure on one file // never stops the others. export const fixMissingThumbnails = async ( lib: Library, client: Client, fileIDs: number[], + found: Map, onProgress?: ProgressCallback, ): Promise => { const log = onProgress ?? (() => {}); @@ -243,31 +245,8 @@ export const fixMissingThumbnails = async ( const api = client.getApiClient(); const { userID } = client.whoami(); - // Resolve each requested fileID to its file record and owning album by - // enumerating the library, each file taken from the first album that holds - // it. The raw `EnteFile` carries the per-file key the thumbnail is - // encrypted under, which the projected records deliberately do not. - const wanted = new Set(fileIDs); - const fileMap = new Map< - number, - { file: EnteFile; collectionName: string } - >(); - for (const album of lib.albums.list()) { - for (const photo of album.photos.list()) { - if (!wanted.has(photo.fileID) || fileMap.has(photo.fileID)) - continue; - const file = lib.getFile(album.collectionID, photo.fileID); - if (file) { - fileMap.set(photo.fileID, { - file, - collectionName: album.name, - }); - } - } - } - for (const fileID of fileIDs) { - const entry = fileMap.get(fileID); + const entry = found.get(fileID); if (!entry) { results.push({ fileID, @@ -279,7 +258,8 @@ export const fixMissingThumbnails = async ( continue; } - const { file, collectionName } = entry; + const { file } = entry; + const collectionName = entry.collection.name; const title = file.metadata.title; const skipReason = reasonToSkip(file, userID); @@ -297,15 +277,10 @@ export const fixMissingThumbnails = async ( const maxSize = file.thumbnail.size!; try { - const photo = lib.photos.byID({ fileID }); - if (!photo) { - throw new Error("file not present in the library cache"); - } - log( `[${collectionName}] Downloading ${title} for thumbnail generation...`, ); - const { path } = await photo.original(); + const { path } = await lib.originalOf(file); const fileBytes = new Uint8Array(readFileSync(path)); if (!isJpeg(fileBytes)) { diff --git a/test/cli/commands.test.ts b/test/cli/commands.test.ts index 78f99fa..2bd5e53 100644 --- a/test/cli/commands.test.ts +++ b/test/cli/commands.test.ts @@ -1353,6 +1353,23 @@ describe("helper fix-missing-thumbnails", () => { ).toBe(1); expect(stderr.text).toBe(last); }); + + // beach.jpg (100) is in Vacation (1) and Work (2), both updated at the same + // time. The fixer names a file after the first album holding it in the + // order of `Library.albums.list()`. On a tie, the album with the higher ID + // comes first, so beach.jpg is named after Work. + it("names a photo in two albums after the same album on an empty cache as on a cache that holds the account", async () => { + const ctx = context(fakeClient({ beachInWork: true })); + const opts = { file: ["100"], json: true }; + const inWork = [{ fileID: 100, collection: "Work", status: "skipped" }]; + expect(await fixMissingThumbnailsCommand(ctx, opts)).toBe(0); + expect(JSON.parse(stdout.text)).toMatchObject(inWork); + + expect(await filesCommand(ctx, undefined, {})).toBe(0); + stdout.text = ""; + expect(await fixMissingThumbnailsCommand(ctx, opts)).toBe(0); + expect(JSON.parse(stdout.text)).toMatchObject(inWork); + }); }); describe("backup-metadata --exif", () => { @@ -1572,11 +1589,11 @@ describe("an ID given on the command line", () => { // An account with many albums (issues #210, #217 and #222). On an empty cache // `collections`, `files` with a collection ID and `get` used to wait for a // refresh that fetched and decrypted the files of every album, one album after -// another. On such an account they seemed to hang. The account here has 40 -// albums of one photo each, encrypted as Ente's clients encrypt them, on the -// fake Ente server of `test/ente-server.ts`. The tests record the path and query -// of each request quak sends. The session is the one logging in to that server -// saves. +// another. So did `helper fix-missing-thumbnails --file` (issue #223). On such +// an account they seemed to hang. The account here has 40 albums of one photo +// each, encrypted as Ente's clients encrypt them, on the fake Ente server of +// `test/ente-server.ts`. The tests record the path and query of each request +// quak sends. The session is the one logging in to that server saves. describe("an account with many albums", () => { const ALBUMS = 40; const PASSWORD = "many-albums"; @@ -1599,7 +1616,9 @@ describe("an account with many albums", () => { // A photo in album `collectionID` as the server sends it, and its original // encrypted as the server stores it. The original's bytes are the title. - // These tests fetch no thumbnail. + // These tests fetch no thumbnail. The server records a thumbnail size, so + // `helper fix-missing-thumbnails` downloads the original before it skips + // the photo as not a JPEG. const photoRow = (photo: { id: number; collectionID: number; @@ -1638,6 +1657,7 @@ describe("an account with many albums", () => { }, file: { decryptionHeader: toBase64(original.header) }, thumbnail: { decryptionHeader: "" }, + info: { thumbSize: 1000 }, updationTime: photo.updationTime, }; return { row, original: original.ciphertext }; @@ -1855,6 +1875,81 @@ describe("an account with many albums", () => { expect(requests).toEqual(["/collections/v2?sinceTime=1"]); }); + // The helper searches the most recently updated album first. All 40 were + // updated at the same time, and on a tie the higher ID comes first. Photo + // 3900 is in album 39, so the helper asks for no file of album 38 or + // earlier. The last request downloads the photo's original. + it("fix-missing-thumbnails --file on an empty cache asks for the albums' files only until one holds the photo", async () => { + const opts = { file: ["3900"], json: true }; + expect(await fixMissingThumbnailsCommand(accountContext(), opts)).toBe( + 0, + ); + + const reason = + "unsupported image format (only baseline JPEG can be regenerated)"; + expect(JSON.parse(stdout.text)).toEqual([ + { + fileID: 3900, + title: "photo-39.jpg", + collection: "Album 39", + status: "skipped", + reason, + }, + ]); + expect(stderr.text).toBe( + "[Album 39] Downloading photo-39.jpg for thumbnail generation...\n" + + `[Album 39] Skipping photo-39.jpg: ${reason}\n`, + ); + expect(requests).toEqual([ + "/collections/v2?sinceTime=0", + "/collections/v2/diff?collectionID=40&sinceTime=0", + "/collections/v2/diff?collectionID=39&sinceTime=0", + "/?fileID=3900", + ]); + expect(existsSync(join(root, "cache", "metadata.json"))).toBe(false); + }); + + // `files` first fills the cache, so the helper waits for a refresh. No + // album has changed since, so the refresh asks for no album's files. + it("fix-missing-thumbnails --file on a cache that holds the account asks for no unchanged album's files, also for a photo in no album", async () => { + const ctx = accountContext(); + expect(await filesCommand(ctx, undefined, {})).toBe(0); + requests.length = 0; + stdout.text = ""; + stderr.text = ""; + + const opts = { file: ["3900"], json: true }; + expect(await fixMissingThumbnailsCommand(ctx, opts)).toBe(0); + const reason = + "unsupported image format (only baseline JPEG can be regenerated)"; + expect(JSON.parse(stdout.text)).toEqual([ + { + fileID: 3900, + title: "photo-39.jpg", + collection: "Album 39", + status: "skipped", + reason, + }, + ]); + expect(stderr.text).toBe( + "[Album 39] Downloading photo-39.jpg for thumbnail generation...\n" + + `[Album 39] Skipping photo-39.jpg: ${reason}\n`, + ); + expect(requests).toEqual([ + "/collections/v2?sinceTime=1", + "/?fileID=3900", + ]); + + requests.length = 0; + stdout.text = ""; + const notFound = { file: ["999"], json: true }; + expect(await fixMissingThumbnailsCommand(ctx, notFound)).toBe(1); + expect(JSON.parse(stdout.text)).toMatchObject([ + { fileID: 999, reason: "file not found in any collection" }, + ]); + expect(requests).toEqual(["/collections/v2?sinceTime=1"]); + }); + it("files counts on stderr each album whose files its refresh fetched", async () => { expect(await filesCommand(accountContext(), undefined, {})).toBe(0); diff --git a/test/cli/log.test.ts b/test/cli/log.test.ts index 1bc1fa6..d89d929 100644 --- a/test/cli/log.test.ts +++ b/test/cli/log.test.ts @@ -234,9 +234,9 @@ describe("-q", () => { }); }); - // No album holds file 999, so fixing it fails. The refresh's progress and - // the list of failed files are left out. The line saying why the command - // failed is the fatal one, and it is printed. + // No album holds file 999, so fixing it fails. The summary from + // `--- Done ---` to the end of the `Failed files:` list is left out. The + // line saying why the command failed is the fatal one, and it is printed. it("prints only the line a failing command exits with", async () => { const fix = ["helper", "fix-missing-thumbnails", "--file", "999"]; expect(await quak("-q", ...fix)).toEqual({ diff --git a/test/cli/read.test.ts b/test/cli/read.test.ts index b60b068..d292041 100644 --- a/test/cli/read.test.ts +++ b/test/cli/read.test.ts @@ -13,6 +13,9 @@ * #217). Neither may wait for a refresh, which would fetch the files of every * collection. On an empty cache, `get` and `get-thumb` ask for that list, then * for one collection's files at a time, until they find the file (issue #222). + * `helper fix-missing-thumbnails --file` does the same until it finds every + * file it names, reading the most recently updated collection first (issue + * #223). * * `collections` and `files` also list in the order the library gives them, not * the albums/photos projection's newest-first order. The fixtures are seeded in @@ -27,6 +30,7 @@ import { freshCollections, freshFiles, freshFile, + freshFilesByID, type FreshReadLibrary, } from "../../src/cli-read.js"; import { fileListRow } from "../../src/cli-output.js"; @@ -253,4 +257,70 @@ describe("CLI read helpers (issue #36 amendment, issue #52)", () => { expect(lib.freshFilesCalls).toEqual([1, 2, 3]); expect(found).toBeUndefined(); }); + + // The server lists collections 1, 2 and 3, and collection 3 was updated + // most recently. File 11 is in collections 1 and 2. + const fileInTwoCollections = (): FakeLibrary => + new FakeLibrary( + [collection(1, 100), collection(2, 200), collection(3, 300)], + [ + file(10, 1, 1_700_000_000_000_000), + file(11, 1, 1_700_000_000_000_001), + file(11, 2, 1_700_000_000_000_001), + file(20, 2, 1_700_000_000_000_002), + file(30, 3, 1_700_000_000_000_003), + ], + ); + + it("freshFilesByID on an empty cache reads the most recently updated collection's files first, until it has found every file", async () => { + const lib = fileInTwoCollections(); + + const found = await freshFilesByID(lib, [11, 20]); + expect(lib.freshCalls).toBe(0); + expect(lib.freshCollectionsCalls).toBe(1); + expect(lib.freshFilesCalls).toEqual([3, 2]); + expect(found.get(11)?.collection.id).toBe(2); + expect(found.get(11)?.file.collectionID).toBe(2); + expect(found.get(20)?.collection.id).toBe(2); + }); + + // Collection 1 was updated last, though collections 2 and 3 have higher + // IDs. File 11 is in collections 1 and 2. + it("freshFilesByID on an empty cache reads the most recently updated collection first, though its ID is the lowest", async () => { + const lib = new FakeLibrary( + [collection(1, 400), collection(2, 200), collection(3, 300)], + [ + file(11, 1, 1_700_000_000_000_001), + file(11, 2, 1_700_000_000_000_001), + ], + ); + + const found = await freshFilesByID(lib, [11]); + expect(lib.freshFilesCalls).toEqual([1]); + expect(found.get(11)?.collection.id).toBe(1); + expect(found.get(11)?.file.collectionID).toBe(1); + }); + + // The stand-in's cache holds what the server holds once `fresh()` has run. + it("freshFilesByID on a cache that holds the collections refreshes, then finds each file in the most recently updated collection that holds it", async () => { + const lib = fileInTwoCollections(); + await lib.fresh(); + + const found = await freshFilesByID(lib, [11, 404]); + expect(lib.freshCalls).toBe(2); + expect(lib.freshCollectionsCalls).toBe(0); + expect(lib.freshFilesCalls).toEqual([]); + expect(found.get(11)?.collection.id).toBe(2); + expect(found.get(11)?.file.collectionID).toBe(2); + expect(found.has(404)).toBe(false); + }); + + it("freshFilesByID on an empty cache reads every collection's files when a file is in none of them", async () => { + const lib = fileInTwoCollections(); + + const found = await freshFilesByID(lib, [10, 404]); + expect(lib.freshFilesCalls).toEqual([3, 2, 1]); + expect(found.get(10)?.collection.id).toBe(1); + expect(found.has(404)).toBe(false); + }); }); diff --git a/test/thumbnails/thumbnails.test.ts b/test/thumbnails/thumbnails.test.ts index bf2be5e..9db0e48 100644 --- a/test/thumbnails/thumbnails.test.ts +++ b/test/thumbnails/thumbnails.test.ts @@ -6,10 +6,12 @@ * working thumbnails, others return 404 or empty bodies. The tests * verify that the detection and repair logic handles each case correctly. * - * As of issue #52 both helpers take an open `Library` for enumeration and the - * `Client` for the API operations that stay unchanged (the thumbnail existence - * check, and the encrypt-and-upload path). `fixMissingThumbnails` reads each - * original through the library's content cache (`photo.original()`). + * `listMissingThumbnails` takes an open `Library` to enumerate the files. Both + * helpers take the `Client` for the API operations: the thumbnail existence + * check, and the encrypt-and-upload path. `fixMissingThumbnails` takes the + * record of each file, which these tests look up with `cachedFilesByID`. It + * reads each original through the library's content cache, by + * `lib.originalOf(file)`. * * `fixMissingThumbnails` is the most complex function in quak: it * downloads the original file, generates a JPEG thumbnail with jpeg-js, @@ -41,6 +43,7 @@ import { import { SRP, SrpServer } from "fast-srp-hap"; import { Client } from "../../src/client.js"; import { Library } from "../../src/library/index.js"; +import { cachedFilesByID } from "../../src/cli-read.js"; import { listMissingThumbnails, fixMissingThumbnails, @@ -656,7 +659,12 @@ describe("fixMissingThumbnails", () => { const client = await login(buildThumbFetch(fixMock)); const lib = await openLib(client); - const results = await fixMissingThumbnails(lib, client, [101]); + const results = await fixMissingThumbnails( + lib, + client, + [101], + cachedFilesByID(lib, [101]), + ); lib.close(); expect(results.length).toBe(1); @@ -693,7 +701,12 @@ describe("fixMissingThumbnails", () => { const client = await login(buildThumbFetch(fixMock)); const lib = await openLib(client); - const results = await fixMissingThumbnails(lib, client, [999]); + const results = await fixMissingThumbnails( + lib, + client, + [999], + cachedFilesByID(lib, [999]), + ); lib.close(); expect(results.length).toBe(1); @@ -715,6 +728,7 @@ describe("fixMissingThumbnails", () => { lib, client, [101, 102], + cachedFilesByID(lib, [101, 102]), (msg, level) => { if (level === "error") errors.push(msg); }, @@ -745,6 +759,7 @@ describe("fixMissingThumbnails", () => { lib, client, [101, 103, 104], + cachedFilesByID(lib, [101, 103, 104]), ); lib.close(); @@ -779,7 +794,12 @@ describe("fixMissingThumbnails", () => { const client = await login(counted.fetch); const lib = await openLib(client); - const results = await fixMissingThumbnails(lib, client, [101]); + const results = await fixMissingThumbnails( + lib, + client, + [101], + cachedFilesByID(lib, [101]), + ); lib.close(); expect(results[0]!.status).toBe("skipped"); @@ -800,7 +820,12 @@ describe("fixMissingThumbnails", () => { const client = await login(counted.fetch); const lib = await openLib(client); - const results = await fixMissingThumbnails(lib, client, [101]); + const results = await fixMissingThumbnails( + lib, + client, + [101], + cachedFilesByID(lib, [101]), + ); lib.close(); expect(results[0]!.status).toBe("skipped"); @@ -841,7 +866,12 @@ describe("fixMissingThumbnails", () => { const client = await login(buildThumbFetch(fixMock)); const lib = await openLib(client); - const results = await fixMissingThumbnails(lib, client, [101]); + const results = await fixMissingThumbnails( + lib, + client, + [101], + cachedFilesByID(lib, [101]), + ); lib.close(); expect(results[0]!.status).toBe("fixed"); -- 2.54.0