# vitaImmich Minimal proof-of-concept [Immich](https://immich.app/) 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/random` and `GET /api/assets/{id}/thumbnail`) - An Immich API key (web UI → Account Settings → API Keys) ## Building Needs [VitaSDK](https://vitasdk.org/) with the `libvita2d`, `curl`, `openssl`, `zstd`, `zlib`, `libpng`, `libjpeg-turbo` and `freetype` packages installed (via `vdpm`). ```sh export VITASDK=$HOME/vitasdk export PATH=$VITASDK/bin:$PATH cmake -B build -S . cmake --build build ``` This produces `build/vitaImmich.vpk`. ## Installing & configuring 1. Copy `vitaImmich.vpk` to the Vita and install it with VitaShell. 2. Launch it once — it creates `ux0:data/vitaimmich/config.txt`. 3. Edit that file (VitaShell → SELECT for FTP makes this easy): ``` server=http://192.168.1.100:2283 apikey=your-immich-api-key ``` If 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.100 ``` This keeps the Host header and TLS SNI on the domain (so the reverse proxy still routes correctly) but connects to the LAN IP directly. 4. 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=` (repeatable, replaces the default scan locations) and `syncmaxmb=` (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.mp4` before 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 transcoded `video/playback` stream 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:picture` and `ux0:video/CAMERA` by default (the standard camera locations), recursing ~2 levels; use `syncdir=` 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. - `deviceId` is hard-coded to "PS Vita" and uploads are not associated with an album.