check / check (push) Waiting to run
`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
151 lines
6.9 KiB
TypeScript
151 lines
6.9 KiB
TypeScript
// How the CLI's read commands obtain current data.
|
|
//
|
|
// `collections`, `files`, `get`, and `get-thumb` must answer for
|
|
// the account's state at the moment the command runs, not for whatever the
|
|
// local cache last happened to hold (owner amendment, issue #36). `get`,
|
|
// `get-thumb` and `files` without a collection ID therefore force a server
|
|
// round-trip through `Library.fresh()` and only then read, so a change made
|
|
// elsewhere is visible. `collections` asks the library for the collection list
|
|
// alone through `Library.freshCollections()` (issue #210): a refresh would also
|
|
// fetch the files of every collection, which on a large account takes long and
|
|
// which `collections` does not print. `files` with a collection ID reads that
|
|
// 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
|
|
// (`listFiles()`). Neither command uses the `albums`/`photos` projection's
|
|
// newest-first order, which re-sorts the rows. The field values still come
|
|
// from each record's raw metadata via `cli-output.ts`. `files` without a
|
|
// collection ID lists every file in the account once (issue #211): the library
|
|
// keeps a record of a file for each collection that holds it, and only the
|
|
// first collection's record is listed.
|
|
|
|
import type { Collection, EnteFile } from "./model/types.js";
|
|
|
|
// The slice of `Library` these helpers read. `Library` satisfies it
|
|
// structurally; a test can drive them with a stand-in that records each fresh
|
|
// read and serves records in a known enumeration order.
|
|
export interface FreshReadLibrary {
|
|
fresh(): Promise<unknown>;
|
|
freshCollections(): Promise<Collection[]>;
|
|
freshFiles(collection: Collection): Promise<EnteFile[]>;
|
|
listCollections(): Collection[];
|
|
listFiles(collectionID: number): EnteFile[];
|
|
getFileByID(fileID: number): EnteFile | undefined;
|
|
}
|
|
|
|
// Every live collection, current as of one collections request, in the
|
|
// server's order.
|
|
export const freshCollections = (
|
|
lib: FreshReadLibrary,
|
|
): Promise<Collection[]> => lib.freshCollections();
|
|
|
|
// The files of one collection, as the server holds them now, in the server's
|
|
// order. `undefined` (not an empty list) when the collection does not exist, so
|
|
// the caller can tell "no such collection" from "an empty collection". Without
|
|
// a collection ID, every file in the account as of a forced refresh, each once,
|
|
// as held by the first collection that holds it.
|
|
export const freshFiles = async (
|
|
lib: FreshReadLibrary,
|
|
collectionID?: number,
|
|
): Promise<EnteFile[] | undefined> => {
|
|
if (collectionID !== undefined) {
|
|
const collections = await lib.freshCollections();
|
|
const collection = collections.find((c) => c.id === collectionID);
|
|
return collection ? lib.freshFiles(collection) : undefined;
|
|
}
|
|
await lib.fresh();
|
|
const listed = new Set<number>();
|
|
const files: EnteFile[] = [];
|
|
for (const collection of lib.listCollections()) {
|
|
for (const file of lib.listFiles(collection.id)) {
|
|
if (listed.has(file.id)) continue;
|
|
listed.add(file.id);
|
|
files.push(file);
|
|
}
|
|
}
|
|
return files;
|
|
};
|
|
|
|
// One file as the server holds it now, or `undefined` when no collection holds
|
|
// it. When the cache holds the account's collections, it refreshes and looks
|
|
// the file up in the cache. On an empty cache, it reads the collection list,
|
|
// then one collection's files at a time, and stops at the first collection
|
|
// that holds the file. That search stores nothing.
|
|
export const freshFile = async (
|
|
lib: FreshReadLibrary,
|
|
fileID: number,
|
|
): Promise<EnteFile | undefined> => {
|
|
// Only a refresh stores collections, so a cache holding any holds them all.
|
|
if (lib.listCollections().length > 0) {
|
|
await lib.fresh();
|
|
return lib.getFileByID(fileID);
|
|
}
|
|
for (const collection of await lib.freshCollections()) {
|
|
const files = await lib.freshFiles(collection);
|
|
const file = files.find((f) => f.id === fileID);
|
|
if (file) return file;
|
|
}
|
|
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<number, { file: EnteFile; collection: Collection }> => {
|
|
const wanted = new Set(fileIDs);
|
|
const found = new Map<number, { file: EnteFile; collection: Collection }>();
|
|
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<Map<number, { file: EnteFile; collection: Collection }>> => {
|
|
// 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<number, { file: EnteFile; collection: Collection }>();
|
|
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;
|
|
};
|