Date a Photo's save path by the membership its record is read from
A Photo kept the first membership of its file, while its record (and so takenAt and year) comes from the most recently synced one. The record projection now keeps that same membership for the Photo, so savePath and isLocal carry the date of takenAt. A test covers one file in two albums whose edited times differ. Model: opus-5-5
This commit is contained in:
+2
-2
@@ -44,8 +44,8 @@ const byNewestAlbum = (a: AlbumRecord, b: AlbumRecord): number =>
|
|||||||
|
|
||||||
// A single photo. Field access mirrors `PhotoRecord`; `record()` returns the
|
// A single photo. Field access mirrors `PhotoRecord`; `record()` returns the
|
||||||
// underlying plain record for callers that need the IPC-safe value. `file` is
|
// underlying plain record for callers that need the IPC-safe value. `file` is
|
||||||
// the file the record was made from, so the save path stays known after a
|
// the membership the record is read from, so the save path carries the date of
|
||||||
// refresh removes the file from the library.
|
// `takenAt` and stays known after a refresh removes the file from the library.
|
||||||
export class Photo {
|
export class Photo {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly rec: PhotoRecord,
|
private readonly rec: PhotoRecord,
|
||||||
|
|||||||
+18
-14
@@ -86,9 +86,9 @@ export interface LibraryChange {
|
|||||||
export interface DerivedRecords {
|
export interface DerivedRecords {
|
||||||
albums: Map<number, AlbumRecord>;
|
albums: Map<number, AlbumRecord>;
|
||||||
photos: Map<number, PhotoRecord>;
|
photos: Map<number, PhotoRecord>;
|
||||||
// One membership of each photo's file, for its `Photo`'s save path. It
|
// The membership each photo's record is read from, for its `Photo`'s save
|
||||||
// holds the file's key, so it stays in this process: no snapshot or change
|
// path. It holds the file's key, so it stays in this process: no snapshot
|
||||||
// carries it.
|
// or change carries it.
|
||||||
files: Map<number, EnteFile>;
|
files: Map<number, EnteFile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +113,20 @@ export const takenAtOf = (file: EnteFile): number =>
|
|||||||
file.metadata.creationTime,
|
file.metadata.creationTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The membership a file's record is read from: the most recently synced, lowest
|
||||||
|
// collection id to break ties.
|
||||||
|
const representative = (memberships: EnteFile[]): EnteFile =>
|
||||||
|
memberships.reduce((best, m) =>
|
||||||
|
m.updationTime > best.updationTime ||
|
||||||
|
(m.updationTime === best.updationTime &&
|
||||||
|
m.collectionID < best.collectionID)
|
||||||
|
? m
|
||||||
|
: best,
|
||||||
|
);
|
||||||
|
|
||||||
// Build one PhotoRecord from every membership of a file. The memberships share
|
// Build one PhotoRecord from every membership of a file. The memberships share
|
||||||
// the same underlying file, so metadata is read from a single representative
|
// the same underlying file, so metadata is read from a single representative;
|
||||||
// (the most recently synced, lowest collection id to break ties); `albumIDs`
|
// `albumIDs` gathers them all.
|
||||||
// gathers them all.
|
|
||||||
const toPhotoRecord = (
|
const toPhotoRecord = (
|
||||||
fileID: number,
|
fileID: number,
|
||||||
memberships: EnteFile[],
|
memberships: EnteFile[],
|
||||||
@@ -124,13 +134,7 @@ const toPhotoRecord = (
|
|||||||
const albumIDs = memberships
|
const albumIDs = memberships
|
||||||
.map((m) => m.collectionID)
|
.map((m) => m.collectionID)
|
||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
const rep = memberships.reduce((best, m) =>
|
const rep = representative(memberships);
|
||||||
m.updationTime > best.updationTime ||
|
|
||||||
(m.updationTime === best.updationTime &&
|
|
||||||
m.collectionID < best.collectionID)
|
|
||||||
? m
|
|
||||||
: best,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pub = rep.pubMagicMetadata ?? {};
|
const pub = rep.pubMagicMetadata ?? {};
|
||||||
const priv = rep.magicMetadata ?? {};
|
const priv = rep.magicMetadata ?? {};
|
||||||
@@ -221,7 +225,7 @@ export const deriveRecords = (
|
|||||||
record.thumbnailPath = paths.thumbnailPath;
|
record.thumbnailPath = paths.thumbnailPath;
|
||||||
}
|
}
|
||||||
photos.set(fileID, record);
|
photos.set(fileID, record);
|
||||||
photoFiles.set(fileID, memberships[0]!);
|
photoFiles.set(fileID, representative(memberships));
|
||||||
takenAtByFile.set(fileID, record.takenAt);
|
takenAtByFile.set(fileID, record.takenAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -416,6 +416,50 @@ describe("Photo save path, local copy, content and EXIF", () => {
|
|||||||
await lib.close();
|
await lib.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dates the save path by the membership its takenAt comes from", async () => {
|
||||||
|
// One file in two albums. The date edited in Ente has reached album 2's
|
||||||
|
// copy, synced later, but album 1 still has an earlier edit.
|
||||||
|
const older = file(1, 1);
|
||||||
|
older.pubMagicMetadata = {
|
||||||
|
editedTime: new Date(2026, 1, 1, 12).getTime() * 1000,
|
||||||
|
};
|
||||||
|
const newer = file(1, 2);
|
||||||
|
newer.updationTime = 2;
|
||||||
|
newer.pubMagicMetadata = {
|
||||||
|
editedTime: new Date(2026, 3, 15, 12).getTime() * 1000,
|
||||||
|
};
|
||||||
|
const client = {
|
||||||
|
whoami: () => ({ email: "u@example.com", userID: USER_ID }),
|
||||||
|
collectionsSince: async (): Promise<CollectionsPage> => ({
|
||||||
|
collections: [collection(1), collection(2)],
|
||||||
|
deleted: [],
|
||||||
|
cursor: 1,
|
||||||
|
}),
|
||||||
|
filesSince: async (args: {
|
||||||
|
collectionID: number;
|
||||||
|
}): Promise<FilesPage> => ({
|
||||||
|
files: [args.collectionID === 1 ? older : newer],
|
||||||
|
deleted: [],
|
||||||
|
cursor: 1,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const lib = await open({ client });
|
||||||
|
const photo = lib.photos.byID({ fileID: 1 })!;
|
||||||
|
|
||||||
|
expect(photo.takenAt).toBe(new Date(2026, 3, 15, 12).getTime());
|
||||||
|
expect(photo.savePath).toBe(
|
||||||
|
join(
|
||||||
|
root,
|
||||||
|
"backup",
|
||||||
|
"2026",
|
||||||
|
"2026-04",
|
||||||
|
"2026-04-15",
|
||||||
|
"2026-04-15.1.jpg",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await lib.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("has a save path, and is not local, without a content source", async () => {
|
it("has a save path, and is not local, without a content source", async () => {
|
||||||
const lib = await open({ contentSource: undefined });
|
const lib = await open({ contentSource: undefined });
|
||||||
const photo = lib.photos.byID({ fileID: 1 })!;
|
const photo = lib.photos.byID({ fileID: 1 })!;
|
||||||
|
|||||||
Reference in New Issue
Block a user