Photos: folders, backup, imports
Photos are delegated to Immich, the way music is to Navidrome. What SoundStorm adds is the part that makes them a household's rather than one account's: everybody has their own folder and sees only their own photos, phones back up into it, and a lifetime of photos can be brought in from Google, Apple and the rest - sorted by when they were taken, with duplicates noticed.
Why photos are delegated
SoundStorm owns a media type only when it is self-describing and needs no transcoding. Photos fail that concretely: iPhones save HEIC, and Go's standard library cannot decode it, so SoundStorm could not even show a thumbnail of most phone photos without a dependency; phone clips need transcoding; and a photo library of any size is the scanning-and-indexing job this project exists not to rebuild. Immich was chosen over lighter options for search by what is in a picture - the feature people compare against Google Photos - at the price of four containers and 6-8 GB of RAM. How it is provisioned and pinned is on the Immich page.
The viewer always shows Immich's preview (a JPEG whatever the original was - a browser can show
neither HEIC nor raw). The original is a download, and a clip plays through the ordinary video
player with seeking, since Immich's playback endpoint honors Range.
Everyone's own photos
Designed by the owner: each member has their own folder, sees only their own photos, and the owner sees everybody's. There is no setting to show anyone the owner's photos; what the owner controls is whether a member has Pictures at all, and how much space their photos may take.
- The folder is
pictures/Personal/<name>/(library.PersonalFolder, the account name made safe as a folder). Everything a member adds to the picture shelf goes there, and so does any phone's backup, the owner's included. A new account whose folder would be somebody else's - or a removed person's, kept - is refused. - Their own Immich account (
provision.PhotoAccountFor, made the first time they look), whose one library reads only their folder. The owner keeps the administrator's account, whose library is the whole pictures folder. - The adapter acts as the person asking (
immich.Config.ActAs): every request, stream and thumbnail carries that person's own key. If a member's account cannot be made it is an error - never a fallback to the administrator's key. - Removing a member deletes their Immich account and keeps their folder, for the owner to decide about.
Space
100 GB each by default (state.DefaultPhotoLimitGB). The owner changes the household
default or one person's limit (or none); the owner's own photos have no limit. Use is measured by
walking the folder, kept for a minute and added to as files land. At the limit an upload, backup or
import gets a 507 saying so - nothing is ever deleted. A member's Settings shows how much is used,
and says plainly that the owner of the server can see their photos.
Phone backup
The server half is two endpoints. POST /api/photos/backup/check
asks "which of these do you have" by name, time taken and size, so a reinstall or a new phone sends
nothing twice. PUT /api/photos/backup?name=&taken= takes one file as the body and
files it under Personal/<name>/<year>/<month>/, keeping a different
file of the same name beside it. These uploads are exempt from the 30-second body deadline, which
would cut off a phone's video on a slow uplink.
The Android half (PhotoBackup.kt) runs as WorkManager jobs, so
Android decides when (Wi-Fi only unless turned off, charging if asked) and it carries on with the app
closed: one when a photo is added, one every six hours, each chaining the next while there is more,
one file at a time with progress through a big one. It sends the original
(setRequireOriginal), because otherwise Android hands over a copy without the location,
which would empty Places and differ in size. The page decides nothing: Settings shows the switch and
its options through a small bridge, and after signing in on a phone the app asks once whether to back
up. Only one backup runs at a time (a lock in the worker), Settings shows the file being sent, and
a job runs in the foreground with a quiet notification where Android allows, since an ordinary job
is stopped at ten minutes.
The iPhone half (ios/SoundStorm/PhotoBackup.swift) answers the same
messages with the same status, so the page has no iPhone code. It reads the photo library (full or
limited access), checks each batch with the server by name and month, writes each original to a
file (iCloud allowed) and sends it in a background URLSession, so a file already going
finishes with the app put away. It runs while the app is open, shortly after a photo is added, and
as a background task iOS starts when it chooses. See iPhone.
?account=); and the server refuses photos meant for someone else. Backup also goes to
the secure name the page was on, never plain http on the Wi-Fi when a secure one exists.Bringing a photo library in
Google and Apple give no way for SoundStorm to pull photos out - Google's Photos API stopped
reading a person's library in 2025, and iCloud never had one - but both hand out a download of
everything as zips. So "Your photos" walks through each service's export, and the zips can be dropped
anywhere on the window. internal/photoimport does the sorting.
- Upload in pieces. 8 MB pieces to
library/.imports/<account>/(POST /api/photos/import,PUT /api/photos/import/{id}?offset=), carrying on from where the server has it after a dropped connection or a reload. - Recognize. The browser reads a zip's own table of contents from its end - zip64 too, as Takeout's 50 GB zips are - without unpacking. A Google or iCloud download, a social network's, or a zip mostly of photos goes to the importer; any other zip is not unpacked and a message says so.
- Sort, one download at a time, a restart resuming, into the person's folder by when each photo was taken. The zip is deleted once sorted.
When was it taken?
In order of trust:
| Source | Notes |
|---|---|
| The download's own record | Takeout's JSON sidecars, found despite Takeout's naming (edited copies, "(1)" copies, names cut at 47 characters, album copies whose sidecar lives in the year's folder); iCloud's "Photo Details.csv", whose date has an unquoted comma; Facebook, Instagram and Flickr JSON. |
| EXIF | Found by its marker, so in a HEIC too (ExifTaken). |
| A date in the name | IMG_20191225_090000, PXL_..., screenshots, WhatsApp's, Telegram's (NameTaken). |
| The file's own date | If not the day of the download. |
| Nothing | Undated/ - never today's month. |
That last rule came from the first walkthrough, which filed a photo under the current month: the iCloud zip finished first and had no date for it, while the Google zip knew it.
Social downloads strip the photos' EXIF, so their JSON is the only record.
photoimport.JSONIndex walks any JSON in a download for two shapes rather than one parser
per network: Facebook's and Instagram's media objects (a uri beside a timestamp, and
sometimes a latitude and longitude), and Flickr's per-photo files. Chats (messages/,
inbox/, Snapchat's chat_media/) are left out - mostly other people's photos -
and so are Snapchat's sticker overlay layers. Cards, cameras, old phones and computers are just a
dropped DCIM or Pictures folder. Only the Facebook format was taken through
end to end; the rest were built from documented shapes and checked with generated downloads.
ErrImplausible), and a download is attempted at most
twice.Sidecars, never the photo
When the date or place came from the download, or the photo has no date of its own, it is written
beside the photo as an XMP sidecar (<file>.xmp). The photo itself is never changed;
Immich reads the sidecar. Checked: a Takeout photo at the Eiffel Tower showed its 2019 date in Paris.
Every upload to the picture shelf - not just imports - is sorted the same way
(savePhoto, library.SaveDecided, which places a file once its bytes have
arrived), with the browser sending the file's own date as a last resort, which on a camera's card is
when it was taken.
Duplicates improve the kept copy
Takeout copies a photo into every album, and the same photo is often in Google, iCloud and the phone's backup. Duplicates are skipped by content (SHA-256, against same-size files already in the folder and within the download). But byte-identical files can arrive with different knowledge around them: in the first walkthrough an iCloud copy had no date and Google's copy did.
So every sidecar records where its date came from (soundstorm:DateSource), ranked
none < the file's own date < a name < Google's or Apple's record < inside the
photo (photoimport.Better). A duplicate with a better-sourced date, or a place where
the kept copy has none, rewrites the kept copy's sidecar and - when the date improved - moves it from
Undated or the wrong month to the right one, to a free name only. Checked on a throwaway
Immich: an undated photo, then Google's copy - moved to the right month, and Immich showed the date
and city.
Managed and arranged folders
Only the dated folders count as SoundStorm's (managedPhoto). A photo that sits only in
a folder somebody arranged and copied in by hand does not stop a copy being filed by date, and is never
improved or moved: their folders are left exactly as they are. Such a photo then shows twice, which
"Your photos" says. Near-duplicates (re-compressed or edited copies) are not caught.
People, Places and On this day
source.PhotoBrowser exposes what Immich works out itself: GET /api/people
(a face is art id person:<id>), a person's photos, GET /api/search/cities
for towns (the id carries city, state and country, since towns share names) and their photos. Somebody
found but unnamed can be named by anyone who can see the photos - a name is for the household.
On this day is SoundStorm's own, not Immich's memories, which are made overnight and only for days Immich picked (empty on a real library). It is one metadata search per earlier year, 25 years back, in parallel, cached an hour; 29 February asks only leap years.
Browsing breaks the title order paging depends on - nobody browses a camera roll alphabetically - so
photos carry a SortKey that is their rank in Immich's newest-first order. Smart search
needs models Immich downloads on first use, so until they arrive the adapter falls back to a filename
search.