Compare commits

..
13 Commits
Author SHA1 Message Date
bobandClaude Fable 5.1 3b3c87e608 Record an album or tag write immich refused
immich answers a bulk album or tag write with HTTP 200 and one result per
asset, so a key without album permission produced "filing 0 asset(s)",
no recorded failure and exit 0. Refusals other than duplicate now come
back from the client and are recorded against the photo and its wiki
pages. The album and tag stages also get separate error handling, so a
failed album write no longer skips tagging for the run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 10:20:42 +01:00
bobandClaude Fable 5.1 fbd77fc91b Remember why photos failed and report the pages without one
Each run now writes .failures.json in the backup directory: the last
reason every photo failed and the wiki pages that use it, dropping any
photo that has since arrived. A new --report mode reads the wiki index
and that file and lists every page whose photo is not on disk, with the
reason or "not yet attempted", calling neither flickr nor immich. That is
what tells a wiki editor which photos have gone from flickr and need
replacing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:45:09 +01:00
bobandClaude Fable 5.1 36c974e843 Slow the whole run down when flickr answers 429
Flickr's limit is an hourly window, so backing off one photo for a couple
of minutes and then hitting the next photo at the old rate just fails
again, which is how a full fetch gave up on photos after six attempts. A
429 on a paced request now doubles the pace for every later flickr call,
up to ten seconds, and the pace relaxes back to the configured value only
after twenty untroubled requests. The per-request backoff also starts at
fifteen seconds instead of five, caps at fifteen minutes, and gets eight
attempts, so a single photo can wait out a short window on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:37:08 +01:00
bobandClaude Fable 5.1 07db2739ca Document setup, running and what lands in immich
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:46 +01:00
bobandClaude Fable 5.1 4e0bafe497 Wire the phases into a command with a summary and exit status
Load only the credentials the requested phases need, run fetch then sync,
and exit 1 when any photo failed so cron mails. A quiet clean run prints
nothing; a quiet failing run prints the failure list at the end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:46 +01:00
bobandClaude Fable 5.1 534d68aac0 Mirror the backup directory into an immich album
List what immich already holds by checksum, upload only what is missing,
then bring every matched asset up to date: the marked description block,
coordinates and capture date where absent, the RGL album, and licence and
photographer tags. Album membership and descriptions are read-compare-write;
uploaded assets are re-read from immich after the album and tag stage so a
duplicate answer or a late metadata job never sees a blank slate. Tag
assignments are re-sent each run and immich reports the duplicates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:46 +01:00
bobandClaude Fable 5.1 a0a5d7c92b Add an immich client that can upload as well as annotate
Carry over immich-sync's client, which lists assets by checksum and drives
albums and tags, and add a hand-rolled multipart upload with the checksum
header so immich can answer duplicate without receiving the bytes twice,
plus a per-asset update for description, date and coordinates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:45 +01:00
bobandClaude Fable 5.1 5549217db5 Fetch every wiki photo at its largest size into the backup directory
Walk the grouped index, ask flickr for each new photo, download the best
size that actually serves, and write the sidecar only once the file is
renamed into place. Photos already complete cost no flickr calls; a change
in which pages use one rewrites just the sidecar. Every failure is
recorded with the pages affected and the run carries on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:45 +01:00
bobandClaude Fable 5.1 2774267425 Build the immich description block and tags from a sidecar
Immich has no copyright or source fields, so the photographer, licence,
flickr page and every wiki page using the photo go into a marked block in
the description. The marker lets a later run find and replace only that
block, leaving anything written by hand alone. Tags mirror licence and
photographer for filtering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:45 +01:00
bobandClaude Fable 5.1 f7884b943e Store each photo's metadata in a json sidecar beside it
The sidecar is what makes the directory a backup on its own and what the
immich phase reads, so it is written atomically and a photo only counts as
done when the sidecar, its file and no .part remain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:44 +01:00
bobandClaude Fable 5.1 fc434a3d9b Ask flickr for each photo's details and its largest size
Two unsigned json calls per photo: getInfo for title, owner, dates and
licence, getSizes for the renditions. Sizes come back ordered best-first so
a missing original can fall through to the next. Not-found and permission
errors become PhotoGone so the run can name the wiki pages affected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:44 +01:00
bobandClaude Fable 5.1 b68d477938 Read the RGL index and group nodes by photo
One json request to the wiki's index carries every node's image fields, so
no per-page scraping is needed. Nodes are grouped by flickr photo id, taken
from the thumbnail filename or the photo page, so a photo used on several
pages is fetched once and its description can list them all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:44 +01:00
bobandClaude Fable 5.1 f05837bd5e Add a retrying HTTP helper shared by every remote call
One place decides what is worth another attempt: 429, 5xx and network
errors, with jittered exponential backoff and Retry-After honoured. Other
4xx responses fail at once. Downloads stream to disk under the same policy,
refuse a body over the size cap, and only flickr calls and photo downloads
are paced; the api key is redacted from debug output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 12:07:44 +01:00
3 changed files with 2789 additions and 9 deletions

No files matched your search

+170
View File
@@ -0,0 +1,170 @@
# rgl-photo-backup
Back up the photos the [Randomness Guide to London](https://london.randomness.org.uk/)
shows on its pages. Nearly all of them live on Flickr, which is a single
point of failure. This fetches the largest available version of each into a
directory, alongside a JSON sidecar of what the wiki and Flickr know about
it, then mirrors the directory into an [Immich](https://immich.app) album
with the source, photographer, licence and wiki pages carried into each
asset's description, tags, capture date and location.
## Requirements
Python 3 and nothing else.
## Setup
Two credentials, both reused from other tools if you already have them:
- `~/.config/flickrtouchr/auth.yml` with a `key:` line holding a Flickr API
key. Public photos need no OAuth, so the key alone is enough. Create one at
flickr.com/services/apps/create if you have none.
- `~/.config/immich/auth.yml` with `url:` (your Immich API base, ending in
`/api`) and `key:` (an API key from Immich's account settings).
Everything else can go in `~/.config/rgl-photo-backup/settings.yml`:
```yaml
directory: /srv/backup/rgl
pace: 1
quiet: true
```
Make the credential files `chmod 600`.
## Running
```
./rgl-photo-backup /srv/backup/rgl # fetch, then sync
./rgl-photo-backup --dry-run /srv/backup/rgl # report only
./rgl-photo-backup --fetch-only --limit 5 DIR # a trial
./rgl-photo-backup --sync-only DIR # disk to Immich only
./rgl-photo-backup --settings other.yml DIR # a different settings file
./rgl-photo-backup --debug DIR # log every request to stderr
./rgl-photo-backup -q DIR # same as --quiet
./rgl-photo-backup --report DIR # which pages have no photo on disk, and why
```
The first run downloads about 4800 photos, one Flickr info call and one
sizes call each, so at the default one-second pace it takes a few hours.
The pace applies to Flickr alone; Immich and the wiki index are not slowed.
A 429 from Flickr doubles the pace for the rest of the run, up to ten seconds
between calls, and it relaxes again after twenty untroubled requests. The
photo that hit the limit is retried with backoff starting at fifteen seconds
and doubling, up to eight attempts.
Later runs skip everything already on disk, and in Immich only write an
album membership or a description that has actually changed.
Exit status is 0 for a clean run, 1 if any photo failed, 2 for a
configuration problem, and 130 if you interrupt it.
### The first run
Take it in stages, so a misconfiguration costs a few photos rather than
thousands:
1. `--fetch-only` on its own, to fill the backup directory.
2. `--sync-only --limit 20`, and look at the twenty assets in Immich.
3. the full run.
Immich queues its own metadata extraction after an upload, so on a first
run a description written straight afterwards can be overwritten by the
extraction finishing. This tool writes descriptions after everything else
for that reason, but if any of them come out wrong, run it again: the next
run compares against what Immich holds and puts them right.
Three things worth checking with curl against your own Immich version
before leaving it unattended, since the API has moved before:
- `POST /api/search/metadata` accepts `withExif`, `withArchived` and `size`
- `POST /api/assets` honours the `x-immich-checksum` header
- `PUT /api/tags` returns `value` as the full slash-separated path
## What lands in Immich
- An album named **RGL** holding every photo. Membership is read first, so
only photos not already in it are added.
- Tags `rgl`, `rgl/licence/<licence>` and `rgl/photographer/<name>`. Unlike
the album and the description, tag assignments are sent every run: Immich
answers an asset that already carries the tag with `duplicate`, and those
are reported as already tagged rather than treated as a failure.
- A description block, appended below anything already there and refreshed
in place on later runs:
```
Randomness Guide to London photo
Photo: <Flickr title>
Source: <Flickr photo page>
Photographer: <name>
Licence: CC BY-SA 2.0 <licence url>
Used on:
<page name> <page url>
```
- The capture date from Flickr and the page's coordinates, only where the
asset has none already.
A photo already in your library, for instance one of your own brought in by
another backup, is recognised by checksum: it joins the album and gets the
block appended rather than being uploaded twice.
## The backup directory
One file per photo, named by Flickr id (`3750465402.jpg`), beside a sidecar
(`3750465402.json`) holding the wiki fields, the Flickr info and the pages
that use it. The twenty-odd photos not on Flickr have no id to use, so they
are named `url-` and the first sixteen hex characters of the SHA1 of their
image URL (`url-0f4b1c2d3e4f5a6b.jpg`), with a matching sidecar. The
directory is a complete backup on its own. A `.part` file is an interrupted
download and is refetched next run.
`.failures.json` records the last reason each photo failed, with the pages
that use it. A photo that later arrives drops out of it. `--report` reads the
wiki index and that file, and lists every page whose photo is not on disk
with the reason, or `not yet attempted`, without calling Flickr or Immich, so
it is the way to find photos that have gone from Flickr and need replacing
on the wiki.
## Running under cron
```cron
23 4 * * * /home/you/bin/rgl-photo-backup
```
With `quiet: true` a clean run prints nothing, so cron only mails when a
photo failed. A quiet run that failed ends with a single `N failed` line
after the individual failures. Failures list the photo and the wiki pages
using it, which is how you learn a Flickr photo has gone.
## Settings
A flag beats the environment, which beats the file, which beats the default.
| Key | Flag | Environment | Default |
|---|---|---|---|
| `directory` | positional | | required |
| `flickr_auth` | `--flickr-config FILE` | | `~/.config/flickrtouchr/auth.yml` |
| `immich_auth` | `--immich-config FILE` | | `~/.config/immich/auth.yml` |
| `index_url` | `--index-url URL` | | the RGL index endpoint |
| `fetch` | `--sync-only` sets false | | `true` |
| `sync` | `--fetch-only` sets false | | `true` |
| `dry_run` | `--dry-run` | | `false` |
| `report` | `--report` | | `false` |
| `limit` | `--limit N` | | unlimited |
| `pace` | `--pace SECONDS` | `RGL_PHOTO_BACKUP_PACE` | `1` |
| `quiet` | `--quiet`, `-q` | | `false` |
| `attempts` | | `RGL_PHOTO_BACKUP_ATTEMPTS` | `8` |
| `backoff` | | `RGL_PHOTO_BACKUP_BACKOFF` | `15` |
| `timeout` | | `RGL_PHOTO_BACKUP_TIMEOUT` | `60` |
| `user_agent` | | `RGL_PHOTO_BACKUP_USER_AGENT` | `rgl-photo-backup/1.0` |
| `debug` | `--debug` | `RGL_PHOTO_BACKUP_DEBUG` | `false` |
`--settings FILE` reads a different settings file; it is a flag only, since
it says where the rest of the settings come from.
## Tests
```
make test # offline unit tests
make lint
```
+1196 -4
View File
File diff suppressed because it is too large. Load diff
+1423 -5
View File
File diff suppressed because it is too large. Load diff