commit f164a263eefe3d68b86d655b28a3815f50276f38 Author: claude Date: Fri Jul 31 23:12:46 2026 +0200 Scaffold enligt new-project-playbook: README, plan, CI-workflow, build-task lyssnarr — musik-discovery på Pi5. Jellyseerr-lik UI, sök (MusicBrainz + YT Music), schemalagda Gemini-förslag från Lidarr-bibliotek + Youtubarr- spellistor, add-to-Lidarr / spela inline / öppna i YT Music. Plan i doc/plan.md — implementation börjar när planen är bekräftad. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..69d9793 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,38 @@ +name: build-and-push + +# Bygger lyssnarr-imagen på Pi5-runnern (arm64, nativt) och pushar till den +# lokala registryn — samma mönster som Archivum. Multi-stage (frontend -> +# backend -> runtime), build-context är repo-roten. Pull:a på Pi5 med +# localhost:5000/lyssnarr:latest (dockge-stacken). +# +# OBS: bygget kör bredvid live-tjänsterna på Pi5 — nice/ionice + BUILD_CPUS=2 +# så boxen förblir responsiv (max 2 tunga byggen samtidigt, se infra-Doc). + +on: + push: + branches: [master] + paths: + - "backend/**" + - "frontend/**" + - "docker/**" + - "package.json" + - ".gitea/workflows/build.yaml" + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build & push image + run: | + nice -n 19 ionice -c 3 docker build --progress=plain \ + --build-arg BUILD_CPUS=2 \ + -f docker/Dockerfile \ + -t localhost:5000/lyssnarr:latest \ + -t "localhost:5000/lyssnarr:${GITHUB_SHA::12}" \ + . + docker push localhost:5000/lyssnarr:latest + docker push "localhost:5000/lyssnarr:${GITHUB_SHA::12}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1aca287 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ +node_modules/ +dist/ +.env +.env.* +*.sqlite3 +data/ +coverage/ +.DS_Store +*.log diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..287d12d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "npm run build", + "group": { "kind": "build", "isDefault": true }, + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..80a29b6 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# lyssnarr + +Self-hosted music discovery for the Pi5 — a Jellyseerr-style web UI that +finds new music for you. Search for artists/albums/tracks, get scheduled +AI-generated suggestions (Gemini) based on what you recently listened to +and what you add to your YouTube Music playlists, and add anything to +Lidarr with one click — or play it inline / open it in YouTube Music. + +See [`doc/plan.md`](doc/plan.md) for the agreed plan and the +[Gitea wiki](https://gitea.brasse-pc.eu/brasse/lyssnarr/wiki) for +architecture notes. + +## Prerequisites (dev box = Arch/Garuda) + +```bash +sudo pacman -S nodejs npm # node >= 20 +``` + +## Build & run + +```bash +git clone gitea-claude:brasse/lyssnarr.git +cd lyssnarr +npm install # installs backend + frontend workspaces +npm run build # -> ./build/ (frontend dist + backend) +npm run dev # dev servers (Vite + Fastify, hot reload) +npm test +``` + +## Runtime configuration + +All secrets live in the Dockge stack `.env` on the Pi5 — never in the +repo. Key variables (full list in `doc/plan.md`): + +| Var | What | +|-----|------| +| `GEMINI_API_KEY` | Google AI Studio key for suggestion generation | +| `LIDARR_URL` / `LIDARR_API_KEY` | Lidarr instance to add music to | +| `SESSION_SECRET` | cookie signing | +| `APP_PASSWORD_HASH` | bcrypt hash for the single-user login | + +## Deploy + +CI (`.gitea/workflows/build.yaml`) builds the arm64 image on the Pi5 +runner and pushes `localhost:5000/lyssnarr:latest`; the Dockge stack +`/srv/dockge-staks/lyssnarr/` runs it behind NPM as +`https://lyssnarr.brasse-pc.eu`. diff --git a/doc/plan.md b/doc/plan.md new file mode 100644 index 0000000..cb334d7 --- /dev/null +++ b/doc/plan.md @@ -0,0 +1,80 @@ +# lyssnarr — plan + +## Goal + +Self-hosted music-discovery web app on the Pi5. A Jellyseerr-style UI +where Björn can (a) search for new artists, albums and tracks, and +(b) get **scheduled** AI-generated suggestions based on two taste +signals: recent listening in the local library (Jellyfin) and tracks +recently added to his YouTube Music playlists (via Youtubarr's data). +Every result can be added to Lidarr with one click, opened in YouTube +Music, or played inline in the app. + +## Stack + +- **Backend:** Node ≥ 20, Fastify, better-sqlite3 (suggestion history + + feedback), node-cron (scheduled generation) +- **Frontend:** Vue 3 + Vite; dark, poster/card-based UI modelled on + Jellyseerr (top search bar, discover grid, action buttons per card); + built to static files served by the backend +- **Container:** single arm64 image, multi-stage Dockerfile + (frontend build → backend deps → slim runtime), mirrors Archivum + +## Deploy target + +Pattern B (container on Pi5): +Gitea Actions on the Pi5 runner → `localhost:5000/lyssnarr:latest` → +Dockge stack `/srv/dockge-staks/lyssnarr/` → NPM vhost +`https://lyssnarr.brasse-pc.eu` (public from day one → the app ships +with login from day one: single user, bcrypt-hashed password from the +stack `.env`, signed session cookie). + +## Integrations + +| What | How | +|------|-----| +| **Lidarr** | REST API (`http://lidarr:8686`): add/monitor artists & albums, check what's already in the library | +| **MusicBrainz** | search + canonical MBIDs (Lidarr adds are MBID-based) | +| **YouTube Music** | unofficial YT Music search from Node → browse/video IDs for "open in YT Music" deep links, thumbnails for the cards, inline playback via YouTube IFrame embed | +| **Gemini API** | suggestion generation; `GEMINI_API_KEY` + model via env | +| **Youtubarr** | read-only mount of its SQLite (`/srv/docker/youtubarr/data/db.sqlite3`) → "recently added to playlists" signal | +| **Jellyfin** | play-history signal — blocked until a music library exists in Jellyfin (milestone 6) | + +## UI sketch (Jellyseerr-like) + +- Top: search bar with artist/album/track scope +- Landing "Upptäck": latest suggestion batch as poster cards, each with + Gemini's one-line motivation +- Card actions: **➕ Lidarr** · **▶ Spela** (inline YouTube embed) · + **↗ YT Music** (deep link) +- Badge on cards already in the Lidarr library +- History page with earlier batches + +## Milestones + +1. **Skeleton end-to-end** — repo, CI, Dockerfile, dockge stack, NPM + vhost, login; an empty authenticated shell reachable on + `lyssnarr.brasse-pc.eu` +2. **Lidarr integration** — library state + add/monitor actions +3. **Search page** — MusicBrainz search enriched with YT Music + thumbnails/links, add/play/open actions +4. **On-demand suggestions** — "generera nu": Gemini prompt built from + Lidarr library + Youtubarr playlist adds +5. **Scheduler + history** — cron-generated batches, history page +6. **Jellyfin signal** — recent music plays feed the prompt (requires + music library in Jellyfin) + +## Roadmap / expansions (proposed — Björn confirms/strikes) + +- 👍/👎 feedback per suggestion, fed into the next Gemini prompt +- Auto-add top suggestions to Lidarr as monitored (configurable) +- Track-level batches exported as M3U playlists into Jellyfin +- Library profile page (genre/decade distribution) +- Webhook/notification when a new batch lands +- Liked Music as a direct signal (ytmusicapi sidecar or Node port) + +## Open questions + +- Gemini model: default `gemini-2.5-flash`, configurable via env — ok? +- Auth: built-in single-user login vs Authentik forward-auth later +- Artwork order: YT Music thumbnails first, Cover Art Archive fallback