Snacks v2.19.0
Automated Media Library Transcoder
This release is all about one thing: the Luna integration. Snacks can now act as a secure, outbound-only connector between your Sonarr/Radarr libraries and Luna, so you can talk to your media library: ask Luna what your collection says about your taste, get recommendations grounded in what you actually own, find the gaps in a series run, and — only if you explicitly allow it — add the winners straight to Radarr or Sonarr from chat or voice. No port forwarding, no public hostname, no inbound tunnel, and a privacy model where paths, API keys, and URLs never leave your box.
✨ Luna integration
Snacks becomes a Luna connector for your enabled Sonarr and Radarr instances. Connect once from Settings → Connections → Luna, and Luna — on the web or iOS, in text chat or voice mode — can see your library the way you'd describe it to a friend and act on it the way you'd trust an assistant to.
What it allows
Read-only, out of the box (with the metadata-read permission):
- "Look through my Radarr library and tell me which genres and directors I return to most."
- "Based on my collection, suggest ten science-fiction movies not already in Radarr. Do not add anything yet."
- "Find completed mystery series that fit my taste and show what Sonarr is missing."
Library changes (separate, explicit opt-in):
- "Add The Matrix to Radarr, monitor it, and start a search."
- "Recommend five movies, explain each choice, and wait for my approval before adding anything."
All of it works in Luna voice mode too — Luna acknowledges the request while Snacks works, then speaks the answer or completion status.
Supported actions
A deliberately small allow-list — task arguments can select ids and ordinary options, but can never supply a URL, API key, path, HTTP verb, or raw request body:
| Capability | Local operation | Returned data |
|---|---|---|
radarr.library.read |
Paged Radarr movie list | title, year, public ids, genres, basic status |
radarr.catalog.search |
Radarr lookup | up to 20 path-free candidates |
radarr.movie.add |
Idempotent add by TMDb id | sanitized added/existing movie |
sonarr.library.read |
Paged Sonarr series list | title, year, public ids, genres, episode counts |
sonarr.catalog.search |
Sonarr lookup | up to 20 path-free candidates |
sonarr.series.add |
Idempotent add by TVDb id | sanitized added/existing series |
Adds are idempotent by public id — asking twice returns the existing entry instead of erroring or duplicating. Unless a task supplies explicit ids, adds use the first accessible Arr root folder and first quality profile; optional root/profile ids can select among your local settings without ever revealing their paths.
Outbound-only by design
Snacks polls Luna for leased tasks (server-suggested cadence, clamped to 2–30s via LunaTaskWorker), executes them against your local Arr APIs, and returns a sanitized result. Nothing connects in: no port forwarding, no reverse proxy, no tunnel, no exposed endpoint. Production Snacks connects only to the pinned official URL https://veryluna.com — custom URLs and plain-HTTP endpoints require explicit env-var overrides that exist solely for the project owner's local integration testing.
Privacy model
- Path-free, always — Arr base URLs, API keys, root-folder paths, file paths/names, and file sizes never enter a task result. Sanitization is structural (the executor builds the payload field by field), not a filter over raw responses.
- Two independent permissions — library metadata reads and library changes are separate toggles on top of the master enable. Snacks rechecks the live settings for every leased task, so removing a permission takes effect immediately — even before Luna receives the next capability heartbeat.
- Credentials handled once — sign-in sends your password to Luna a single time and never writes it to disk; only the scoped refresh session is persisted (via the secrets store, tightened to owner read/write on Unix). Disconnecting revokes the token server-side and deregisters the connector so Luna shows it offline immediately.
- Nothing lingers — completed remote-task rows are hard-deleted the moment Luna consumes their result, a five-minute safety prune removes abandoned rows, and disconnecting or revoking deletes unfinished tasks. The connection can be reviewed or revoked any time from Luna's Settings → Connected apps on web or iOS.
- Hard result bounds — 200-row library pages, 20 search candidates, and a 16MB cap on any single Arr response keep one remote task from buffering or exfiltrating a whole library.
Surfaces
- Settings → Connections → Luna — enable polling, opt into reads/changes, sign in, and watch live connection status (online/offline, last error, account) without leaving the page.
- Docs — a full Luna section in
/docs/index.htmland docs/LUNA_INTEGRATION.md covering setup, the capability table, the privacy contract, and example prompts.
Tests
LunaIntegrationTests(new) — library reads are paginated and structurally path-free; adds use local defaults but never return the root path; existing-series adds are idempotent; capabilities follow each explicit privacy permission; production accepts only the official URL (local testing requires both env overrides); connect persists only the scoped session and polls outbound.- Electron
docs.test.cjs/frontend.test.cjs— the docs page carries the Luna section and the integrations panel wires the Luna connect/permission flows.
Files Changed
Luna integration
Snacks/Services/LunaConnectionService.cs(new) — session lifecycle (connect/refresh/disconnect/revoke), URL policy pinned tohttps://veryluna.com, outbound task-lease polling, scoped-secret persistenceSnacks/Services/LunaTaskExecutor.cs(new) — the sanitized task allow-list: paged library reads, catalog search, idempotent adds; result bounds and typed error codesSnacks/Controllers/LunaConnectionController.cs(new),Snacks/Models/LunaConnectionState.cs(new),Snacks/Models/IntegrationConfig.cs—LunaIntegrationconfig (enable, reads, changes), token-free status APISnacks/Program.cs—LunaTaskWorkerbackground service and the redirect-disabledLunaConnectorHTTP clientSnacks/Services/ConfigFileService.cs— scoped-secret save path used by the Luna sessionSnacks/Views/Shared/_IntegrationSettings.cshtml,Snacks/wwwroot/js/settings/panels/integrations-panel.js,luna-status.js,Snacks/wwwroot/js/api.js,Snacks/wwwroot/css/site.css— the Luna settings panel and live statusdocs/LUNA_INTEGRATION.md(new),Snacks/wwwroot/docs/index.html— full integration documentation
Tests
Snacks.Tests/Integration/LunaIntegrationTests.cs— Luna privacy, capability, URL-policy, and session suite (new)electron-app/tests/docs.test.cjs,frontend.test.cjs— Luna docs and settings-panel coverage
Version bumps
Snacks/Snacks.csproj—<Version>2.19.0</Version>(single source of truth)electron-app/package.json/package-lock.json,build-and-export.bat,Snacks.Tests/Settings/AppVersionTests.cs,Snacks/wwwroot/docs/index.html— synchronized viasync-version.mjs
Full documentation: README.md · /docs/index.html on a running instance