Commit Graph
11 Commits
Author SHA1 Message Date
sneak fc06b9100b Have photo.download() write to the save path of the Photo's own file copy
check / check (push) Successful in 4m9s
Photo.download() now hands the content cache the file copy the Photo
holds, the one savePath and isLocal are dated from, instead of the
fileID the cache then looked up in the store. A Photo held across a
refresh that brings a date edited in Ente is now downloaded to the path
its savePath names. A test covers that case.

Model: opus-5-5
2026-10-01 21:01:33 +00:00
sneak 8992c7c5a9 Date download() and backup by the album copy photo.savePath uses
Export `representative` from records.ts: the album's copy of a file that
its record is read from. `store.getFileByID` now returns that copy, so
`download()` writes under the date `photo.savePath` names. `lib.backup()`
takes each file's copy by the same rule from all of its albums, and every
album links that one path. Tests cover one file in two albums whose copies
carry different edited dates, for `download()` and for `lib.backup()`.

Model: opus-5-5
2026-10-01 20:56:39 +00:00
sneak 2323da4727 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
2026-10-01 20:56:39 +00:00
sneak 4c72e05002 Keep a Photo's save path after its file leaves; refuse an empty download directory
A Photo now keeps the file its record was made from (the record
projection holds one membership of each file), so savePath and isLocal
still answer after a refresh removes the file.

Library.open rejects an empty downloadDirectory.

A backup clears leftover temp files in every date folder under its
directory, not only in those of the files in its scope.

placeOriginal no longer deletes what is at the save path before copying
a live photo, and the tests that depended on that are removed.

The TODO.md entry for issue 143 describes only the current layout.

Model: opus-5-5
2026-10-01 20:56:39 +00:00
sneak bab19660f5 Save originals at photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; add photo.download() (closes #143)
Each original's save path is now
YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<fileID><ext> under the library's
download directory, dated by takenAt in the machine's time zone. The
directory defaults to photos/ in the working directory, resolved once at
open. savePath is always a string, with or without a content cache, and
isLocal is true only when the original is there.

photo.download() puts the original at its save path: copied from the
cache when the cache holds it, fetched straight there otherwise.
lib.backup() uses the same code for each file, writes each file's JSON
beside it, and links collections/ to the save paths. The backup has no
originals/ folder.

Model: opus-5-5
2026-10-01 20:56:39 +00:00
clawbot 67d554fb46 exif(): read HEIF/HEIC originals with exifreader (closes #145)
check / check (push) Successful in 2m10s
`photo.exif()` and `quak backup-metadata --exif` now read EXIF through `exifreader`. HEIC/HEIF originals get EXIF, including a live photo's image, as do the other formats `exifreader` reads. It replaces `exif-reader` and the hand-written JPEG scan. `PhotoExif` is unchanged.

The `backup-metadata` dump now holds `exifreader`'s tag output, with unnamed tags keyed `undefined-` plus their number. GPS altitude without a reference counts as above sea level. A latitude or longitude without its hemisphere tag, an unreadable text tag, and a date the parser rejects each give no field.

Licence: `exifreader` is MPL-2.0, used unmodified.

Model: opus-5-5
2026-10-01 22:34:09 +02:00
clawbot 2b598d3622 Photo: save path, is-local, content bytes, metadata and EXIF getters (closes #141)
check / check (push) Successful in 1m24s
`Photo` gains:

- `savePath` and `isLocal`: synchronous, disk only. Where `lib.backup()` writes the original under the library's `downloadDirectory`, and whether all of it is there; a copy only in the cache does not count.
- `content()` and `exif()`: async, may download. `exif()` reads the common EXIF fields of a JPEG and returns `{}` for anything else.
- The getters `modifiedAt` and `hash`, also on `PhotoRecord`, and `year`.

For a live photo the backup has not stored yet, `savePath` carries the title's extension, and the image may be stored under a different one. The JPEG EXIF scan moved to `src/exif.ts`. The exported `PhotoContent` interface gains `savePath` and `isLocal`.

Judgement call: `iso` is read only when the file stores it as a single number.

Model: opus-5-5
2026-10-01 17:58:23 +02:00
clawbot 9e6deb21eb Store live photos as their image and their video (closes #107)
check / check (push) Successful in 41s
A live photo, which Ente stores as one ZIP, is unpacked as it downloads
into its image and its video, each `<fileID>.<ext>` with its extension
from the ZIP, beside `<fileID>.livephoto.json`, which names the two.
Both are checked against the recorded hash and renamed into place only
when both are complete. A ZIP with a second image or video, or whose
parts come to more than 20 times its size plus 16 MiB, is refused. The
backup and the content cache count a live photo as stored only with both
files, album folders link both, `quak get` writes both, and the content
result gives the video as `videoPath`. A ZIP an earlier version stored
is replaced.

Model: opus-5-5
2026-09-28 18:06:01 +02:00
clawbot f1836ced57 Fix two intermittently failing library tests (closes #90)
check / check (push) Successful in 27s
Library.close() now returns a promise that resolves once the work it
started has finished: an in-flight refresh with its cache write, the ML
data fetch, and running precache sweeps. The interval test could see a
refresh's new state, close, and remove the directory while the write was
still running. Every library test and CLI command now awaits close(), and
tests hold each of the three writes open to prove close() waits for it.

The precache test waited for its stub source to be called, but the cache
records a file only after checking it on disk, so status() could lag. It
now waits for both fills to report "done".

Model: opus-5-5
2026-09-23 04:38:00 +02:00
clawbot 2e00139d3c Precache all thumbnails and pinned originals inside open() (closes #48)
check / check (push) Successful in 26s
Precaches aggressively inside open(): every thumbnail (newest first, through the shared thumbnail pool, never evicted; visible/ahead requests preempt the background fill) and the pinned originals — the favorites album plus every file within precacheOriginalsDays (default 7) of the newest takenAt — through the content pool. The pinned set integrates with the #47 eviction hook; when the window moves or a favorite is removed, files become ordinary evictable originals. open() options precacheThumbnails/precacheOriginals/precacheOriginalsDays; progress via onProgress/status().

Model: opus-4-8
2026-09-22 21:21:34 +02:00
clawbot 5db59a6e2b On-disk content and thumbnail cache with per-photo fetch and prefetch (closes #46)
check / check (push) Successful in 33s
Adds the on-disk content and thumbnail cache keyed by fileID: originals/ and thumbnails/ under cacheDirectory, present-means-complete (streaming atomic rename), orphan temp reaping on open. Photo.original/thumbnail return a cached path with no network when present, else fetch through the shared request pool; thumbnails.ensure drives the thumbnail pool with priority, dedup and AbortSignal. One shared RequestPools set serves both the ML fetch and the content cache. Content-hash integrity is deferred (#68); authenticated streaming decrypt guarantees integrity now.

Model: opus-4-8
2026-09-22 18:38:22 +02:00