- Left stick pans (proportional, in screen px) and the right stick up/down zooms, mirroring the detail view's feel; d-pad and L/R triggers still work. - Cluster gallery: a member is placed on the grid only once its date/aspect have resolved, so it drops into its final month section instead of showing as an "unknown" placeholder that reflows the whole grid as metadata arrives. Show "Loading photos..." until the first members come in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uKJQMYDE9Nrq9ECsU3MxH
vitaImmich
Minimal proof-of-concept Immich client for the PS Vita. It connects to your Immich server and shows your library (photos and videos) as a scrollable chronological grid (newest first), with a full-screen viewer and hardware-accelerated video playback.
Requirements
- A PS Vita with HENkaku/h-encore and VitaShell
- An Immich server reachable from the Vita's Wi-Fi (recent Immich version —
it uses
POST /api/search/randomandGET /api/assets/{id}/thumbnail) - An Immich API key (web UI → Account Settings → API Keys)
Building
Needs VitaSDK with the libvita2d, curl, openssl,
zstd, zlib, libpng, libjpeg-turbo and freetype packages installed
(via vdpm).
export VITASDK=$HOME/vitasdk
export PATH=$VITASDK/bin:$PATH
cmake -B build -S .
cmake --build build
This produces build/vitaImmich.vpk.
Installing & configuring
-
Copy
vitaImmich.vpkto the Vita and install it with VitaShell. -
Launch it once — it creates
ux0:data/vitaimmich/config.txt. -
Edit that file (VitaShell → SELECT for FTP makes this easy):
server=http://192.168.1.100:2283 apikey=your-immich-api-keyIf your server sits behind a public domain (reverse proxy) and your router does not support NAT loopback, add the server's LAN IP so the domain is pinned to it while on your home network:
server=https://immich.example.com apikey=your-immich-api-key serverip=192.168.1.100This keeps the Host header and TLS SNI on the domain (so the reverse proxy still routes correctly) but connects to the LAN IP directly.
-
Relaunch the app.
Controls
Grid view:
| Button | Action |
|---|---|
| D-pad | Move selection (hold to repeat) |
| L / R | Jump two rows up / down |
| X | Open item full screen |
| SELECT | Open the sync overview |
| START | Exit |
Full-screen view:
| Button | Action |
|---|---|
| Left / Right | Previous / next item |
| X | Play video / retry a failed photo |
| O | Back to grid |
| SELECT | Open the sync overview |
| START | Exit |
Sync overview (SELECT):
| Button | Action |
|---|---|
| X | Queue all local-only files to upload |
| O / SELECT | Back |
| START | Exit |
Video playback:
| Button | Action |
|---|---|
| X | Pause / resume |
| Left / Right | Seek ±10 seconds |
| O | Stop and go back |
More of the library is fetched automatically (100 photos at a time, newest
first) as you scroll toward the end. Errors are shown on screen and logged
to ux0:data/vitaimmich/log.txt.
Syncing the Vita's camera media
On startup the app mounts the Photos app's storage (photo0:, i.e. the
ACL-protected ux0:picture where camera shots and recordings live) via
sceAppMgrAppDataMount, scans it (plus ux0:video/CAMERA, a few levels
deep; .jpg/.jpeg/.png/.mp4) and merges those files into the same
date-sorted grid as your server library.
Files larger than 512 MB are skipped so a movie collection in ux0:video
is never hashed or offered for upload. Both knobs live in config.txt:
syncdir=<folder> (repeatable, replaces the default scan locations) and
syncmaxmb=<MB> (the size cap; 0 disables it).
A small badge in the top-left corner of each cell shows its status:
- blue-grey dot — cloud only (a server asset not present on the Vita)
- orange up-arrow — local only (on the Vita, not yet on the server)
- blinking orange — queued / uploading
- green check — backed up (exists on both; shown once, as the server asset)
- red dot — failed (see the sync overview for the reason)
A background thread hashes each local file (SHA1) and asks the server which
ones already exist (POST /api/assets/bulk-upload-check). Hashing and the
duplicate check start automatically. Uploads do not start on their own:
open the sync overview with SELECT and press X to queue every
local-only file. Each upload is a POST /api/assets multipart request with
an x-immich-checksum header for fast server-side dedup; progress is shown
in the overview.
PoC limitations
- TLS certificate verification is disabled (no CA bundle is shipped), so prefer plain HTTP on a trusted LAN or treat HTTPS as unverified.
- Thumbnails stream in on a background thread (one at a time over a keep-alive connection); cells show a grey placeholder until loaded.
- Videos are downloaded in full to
ux0:data/vitaimmich/video.mp4before playback (deleted afterwards), so they need free space on the memory card and a moment to start. Playback uses the Vita's hardware decoder via SceAvPlayer, so only MP4 (H.264/AAC) plays — Immich's transcodedvideo/playbackstream is used, which is H.264 with default server settings. The decoder rotates four output frame buffers (~13 MB CDRAM at 1080p) so async GPU rendering can't be torn by a recycled frame. - Caps at 1000 server assets and 500 local camera files per launch; no albums or search.
- Local media sync is one-way (Vita → server). It scans
ux0:pictureandux0:video/CAMERAby default (the standard camera locations), recursing ~2 levels; usesyncdir=in the config to scan other folders. - Local video grid cells show a poster thumbnail (with the VIDEO badge): the first frame is decoded with SceAvPlayer on the background loader thread (NV12 -> RGB, downscaled), the same handoff as photos. Poster extraction and playback share the single hardware decoder, so a poster is never decoded while a video is playing (and vice versa).
- Hashing 500 files at startup takes a while; it runs in the background so browsing is never blocked, but statuses fill in gradually.
deviceIdis hard-coded to "PS Vita" and uploads are not associated with an album.