# MeshBay — Photos application (design) > Status: **proposal**, not implemented. Read `docs/apps.md` first — Photos is > a new group application built on the plug-in mechanism described there. > Read `docs/mediacenter.md` and `docs/musicbay.md` second: Photos reuses > their node-side pattern (thumbnails generated and cached by the node, > delivered through the existing chunk path, `IndexEntry` gains a few more > additive fields) wherever the same shape applies, and this document states > only where Photos differs and why. > > Follows the project convention: every claim names the adversary it holds > against (§8). --- ## 0. What was asked, in one paragraph A group "application" with the same principles as Videos/Music — a view over the existing file index, no catalogue, enable/disable per group on the same signed-op mechanism — for a shared photo library: the classic photo-album elements (album grid, next/previous within a folder, a lightbox), a small button to download a photo folder as a zip (Files already has this), and optional per-photo info read from the image's own EXIF data. Three things are explicitly **not** wanted, and they are what makes Photos smaller than Videos, not bigger: **several** root folders rather than one, a single album-grid view rather than a mode toggle with a "flat" fallback, and no third-party service at all. --- ## 1. What this design does not reopen Everything Videos/Music already established stands, and this plan fits inside it: - **Views over the index, never a catalogue** (`desktop-client-v1.md` §6.10, draft-v6 §2.7). A file stays tied to its filesystem representation; an album is a directory, exactly as a season is a folder in Videos. - **The apps plug-in mechanism** (`apps.md`): a new `photos-app.js`, one registry entry, one node-side `ALLOWED_APPS` entry, i18n keys, the asset list, the two file-set tests. Enablement is a per-group, operator-signed setting, same shape as `member_upload`/`apps_enabled`. - **Group-related server state lives on the node** (E9). Nothing here puts a row on the hub. - **Node-side derived-data caching, never in a shared root** — thumbnails live in `data_dir/media_cache.db`, the same file Videos and Music already use, never beside the originals. - **Filesystem portability** (§6.8) — nothing here writes into a shared root. - **`thumb_hash`/`width`/`height` on `IndexEntry` are already generic**, not video-specific despite their current comments (`protocol.py:166`) — Photos populates them exactly like Videos does, no new delivery mechanism. --- ## 2. Where Photos differs from Videos/Music, and why ### 2.1 Several roots, not one Videos and Music each gate on a single `video_root`/`audio_root` — one folder, because their expensive work (TMDB/MusicBrainz lookups) needed an explicit, deliberate opt-in and a real media library is usually one tree. A photo library is routinely scattered: a "Vacances" folder here, a "Famille" folder there, an old "Scans" folder from a different import, none of them nested inside a common parent that would make sense to expose whole. **Photos takes a *set* of root folders**, each independently chosen, each independently removable. - New per-group setting: `photo_roots` — a JSON list of root-relative paths, stored the same way `enabled_apps` already is (`roster.py`, `SETTING_ENABLED_APPS`'s own `json.dumps(sorted(...))` pattern): ```python SETTING_PHOTO_ROOTS = "photo_roots" async def photo_roots(self, group_id: str) -> list[str]: value = await self.get_setting(group_id, self.SETTING_PHOTO_ROOTS) if value is None: return [] try: return list(json.loads(value)) except (ValueError, TypeError): return [] async def set_photo_roots(self, group_id: str, roots: list[str], set_by: str = "") -> list[str]: await self.set_setting(group_id, self.SETTING_PHOTO_ROOTS, json.dumps(sorted(roots)), set_by) return roots ``` Empty list means "nothing configured yet" — same "absent means show nothing" discipline `underVideoRoot` already established, not "the whole index": the node runs no thumbnail/EXIF work for a group before at least one root exists either (§2.3's enrichment gate), so falling back to everything would show files nothing has enriched. - New signed op, same shape as `apps_enabled` (a *set*, not a single value, signed in one message rather than one op per root — adding three folders in Settings costs one signature): ``` photo_roots { roots: [...] } # client → node photo_roots_ack { roots: [...] } # node → every connected peer ``` `OP_PHOTO_ROOTS = "photo_roots"` in `adminop.py`, subject = the sorted, comma-joined root list — identical convention to `apps_enabled`'s subject, so the operator's browser and the node arrive at identical bytes to sign/verify without inventing a second serialization. - **Validation happens before a signature is ever asked for**, same principle as `apps_enabled`'s "empty set refused up front" and `video_root`'s path check: every candidate path is resolved against the group's actual `RootSet` and must name a real, currently-readable directory, or the whole request is refused immediately — one bad path in a batch of five never reaches the operator's browser as a signing prompt. Unlike `apps_enabled`, an **empty** `roots` list is accepted (it is the "nothing configured yet" state, not a lockout — there is no Photos-equivalent of "the operator would be locked out of their own group" to guard against here). - Broadcast in `handshake_ack` next to `video_root`/`audio_root` (`"photo_roots": list(self._group_ctx().get("photo_roots") or [])`), and `photo_roots_ack` to every already-connected peer on change, same as `video_root_ack`. - **No root may be nested inside another already-configured root** — same rule §6.7 of `desktop-client-v1.md` already applies to a group's *named* roots, applied here one level down to avoid double-listing the same directory's images once directly and once as part of a parent. Checked case-insensitively (§6.8), at validation time, alongside the real-directory check. ### 2.2 UI: an add/remove list, not a single `` built from `rootFolderOptions`, one path, confirm-on-change) does not fit a *set*. Photos gets its own small component: the same `rootFolderOptions` `