Compare commits

...
10 Commits
Author SHA1 Message Date
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 2450 additions and 1 deletions

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
# 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
```
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.
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.
## 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` |
| `limit` | `--limit N` | | unlimited |
| `pace` | `--pace SECONDS` | `RGL_PHOTO_BACKUP_PACE` | `1` |
| `quiet` | `--quiet`, `-q` | | `false` |
| `attempts` | | `RGL_PHOTO_BACKUP_ATTEMPTS` | `6` |
| `backoff` | | `RGL_PHOTO_BACKUP_BACKOFF` | `5` |
| `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
```
+1076 -1
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff