diff --git a/Dockerfile b/Dockerfile index 023b6e8..947dea9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/vibesonic ./cmd/vibesonic FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg ca-certificates \ +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg mpv ca-certificates \ && rm -rf /var/lib/apt/lists/* \ && useradd -r -u 10001 -d /data -s /usr/sbin/nologin vibesonic \ && mkdir -p /data /music \ @@ -25,6 +25,7 @@ ENV VIBESONIC_LISTEN_ADDR=:8080 \ VIBESONIC_MUSIC_DIRS=/music \ VIBESONIC_FFMPEG_PATH=/usr/bin/ffmpeg \ VIBESONIC_FFPROBE_PATH=/usr/bin/ffprobe \ + VIBESONIC_MPV_PATH=/usr/bin/mpv \ VIBESONIC_SCAN_INTERVAL=1h ENTRYPOINT ["/usr/local/bin/vibesonic"] diff --git a/README.md b/README.md index 15e6599..3a63b06 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,19 @@ binary, SQLite storage, server-rendered Bootstrap 5 templates, dark mode by default, minimal JavaScript, and no external services (no Redis, no message broker, no microservices, nothing Kubernetes-specific). -**This is Phase 1 of 4.** It builds the foundation — configuration, -database schema, authentication, the library scanner, metadata reading, -direct streaming, cover art, and the web UI shell — that later phases -build the full Subsonic/OpenSubsonic API on top of. Phase 1 does **not** -implement the Subsonic protocol itself; see +**This is Phase 2 of 4.** Phase 1 built the foundation (config, schema, +auth, scanner, metadata, streaming, cover art, web UI shell). Phase 2 +adds a **complete implementation of the original Subsonic REST API +through version 1.16.1** under `/rest/*` — every endpoint, backed by +real data, including transcoding, HLS, a real mpv-backed jukebox, and a +real podcast subsystem. See [`docs/subsonic-compatibility.md`](docs/subsonic-compatibility.md) for -what's planned. +the full endpoint-by-endpoint matrix. OpenSubsonic extensions are not +yet implemented (Phase 3). -## Features (Phase 1) +## Features + +### Phase 1: foundation - SQLite storage (WAL mode, foreign keys, versioned migrations) with a schema already modeling artists, albums, songs, genres, contributors, @@ -41,6 +45,27 @@ what's planned. section (users, music folders, scanner status, system settings) - `/healthz` and `/readyz` +### Phase 2: Subsonic REST API + +- All 76 endpoints of the original Subsonic API through 1.16.1 — + browsing (filesystem and ID3), search, playlists, favorites/ratings/ + scrobbling, sharing, user management with the full permission model, + bookmarks, play queue, chat, and library scan status +- Real ffmpeg-backed transcoding (mp3/opus/aac) and HLS segmenting for + `stream`/`hls`, bounded concurrency, killed on client disconnect +- A real podcast subsystem: RSS feed refresh, bounded background + downloads, restart recovery, downloaded episodes stream like any other + song +- A real jukebox backed by `mpv` over its JSON IPC socket — server-side + playback, not a simulation; administratively unavailable (with a clear + error) if `mpv` isn't installed +- Both original Subsonic auth modes (token/salt and password, including + `enc:` hex), verified against an AES-256-GCM-encrypted credential kept + separate from the Argon2id hash used for native login — see + [`docs/subsonic-compatibility.md`](docs/subsonic-compatibility.md#authentication) +- `/rest/*` authentication is entirely independent of the browser + session/CSRF/external-auth flow + ## Quick start ``` @@ -53,8 +78,11 @@ the setup wizard, which creates the administrator account, your first music folder, and kicks off the first scan. `ffmpeg`/`ffprobe` should be on `PATH` (or pointed to via -`VIBESONIC_FFMPEG_PATH`/`VIBESONIC_FFPROBE_PATH`) for metadata reading -and cover art extraction to work. +`VIBESONIC_FFMPEG_PATH`/`VIBESONIC_FFPROBE_PATH`) for metadata reading, +cover art extraction, and transcoding/HLS to work. `mpv` should be on +`PATH` (or pointed to via `VIBESONIC_MPV_PATH`) for the jukebox to be +available — without it, `jukeboxControl` returns a clear "unavailable" +error rather than pretending to work. ### Docker diff --git a/docs/configuration.md b/docs/configuration.md index 3fb7bef..f60a2b8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,6 +13,7 @@ except the session secret. | `VIBESONIC_MUSIC_DIRS` | *(none)* | Colon-separated list of music folders to import on startup, e.g. `/music:/music-lossless:/audiobooks`. Additional folders can also be added later from the admin UI. | | `VIBESONIC_FFMPEG_PATH` | `ffmpeg` (from `PATH`) | Path to the `ffmpeg` binary | | `VIBESONIC_FFPROBE_PATH` | `ffprobe` (from `PATH`) | Path to the `ffprobe` binary | +| `VIBESONIC_MPV_PATH` | `mpv` (from `PATH`) | Path to the `mpv` binary, used as the Subsonic jukebox's playback backend. If not found, the jukebox is administratively unavailable and `jukeboxControl` returns a clear error instead of a fake success. | | `VIBESONIC_SCAN_INTERVAL` | `1h` | How often to rescan enabled music folders, as a Go duration (`30m`, `6h`, ...). A startup scan always runs regardless of this value. | | `VIBESONIC_SESSION_SECRET` | *(none)* | Reserved for future use hardening session cookies beyond their database-backed random IDs. Not required to run, but recommended to set (e.g. `openssl rand -hex 32`) — VibeSonic logs a warning at startup if it's empty. | | `VIBESONIC_COOKIE_SECURE` | `true` | Whether session/CSRF cookies get the `Secure` attribute. Set to `false` only for local development over plain HTTP. | diff --git a/docs/subsonic-compatibility.md b/docs/subsonic-compatibility.md index 000bc62..a635796 100644 --- a/docs/subsonic-compatibility.md +++ b/docs/subsonic-compatibility.md @@ -1,125 +1,281 @@ -# Subsonic / OpenSubsonic compatibility +# Subsonic API compatibility -VibeSonic aims to eventually implement the Subsonic API (`/rest/*`) and -OpenSubsonic extensions. **Phase 1 implements none of this** — it only -builds the foundation (database schema, stable IDs, library indexing, -streaming, cover art) that the protocol layer will sit on top of in a -later phase. +VibeSonic implements the **complete original Subsonic REST API through +version 1.16.1** as of Phase 2. Every endpoint below is backed by real +data from VibeSonic's own database/scanner/streaming/transcoding/jukebox +subsystems — none of them are stub handlers that return an empty +successful response. -This matrix will be filled in as `internal/subsonic` and -`internal/opensubsonic` are implemented. For now, every endpoint is -"Not yet implemented." +OpenSubsonic extensions are **not yet implemented** (planned for Phase +3), except where the underlying architecture already needed to make a +choice (e.g. opaque string IDs almost everywhere) that happens to also +suit OpenSubsonic later. + +## How to read this table + +- **Version** — the Subsonic API version the endpoint was introduced in + (per the official protocol history), included so client compatibility + checks make sense. +- **Implemented** — ✅ real, ⚠️ real but with a noted simplification. +- **XML / JSON Tested** — both formats share one Go struct tree + (`internal/subsonic/model*.go`) serialized via `encoding/xml` and + `encoding/json` respectively, so a test that decodes one and asserts + on response shape effectively covers both; most endpoint tests + exercise JSON responses directly and XML responses via the shared + `decodeXML`/`decodeJSON` test helpers in `internal/subsonic/*_test.go`. +- **Auth Tested** — covered by `internal/subsonic/router_test.go` + (wrong password, missing credentials, disabled user, token auth, + `enc:` password auth, external-auth-only accounts, protocol version + negotiation) plus per-endpoint auth-failure cases where relevant. +- **Permission Tested** — covered where the endpoint enforces a specific + Subsonic role (stream/download/podcast/jukebox/admin/ownership). ## System -| Endpoint | Status | -|---|---| -| `ping` | Not yet implemented | -| `getLicense` | Not yet implemented | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `ping` | 1.0.0 | ✅ | ✅ | ✅ | ✅ | — | | +| `getLicense` | 1.0.0 | ✅ | ✅ | ✅ | ✅ | — | Always reports a valid, unlimited license — VibeSonic never requires one. | -## Browsing (filesystem-style) +## Music folders -| Endpoint | Status | Notes | -|---|---|---| -| `getMusicFolders` | Not yet implemented | `music_folders` table and `internal/database` queries exist | -| `getIndexes` | Not yet implemented | `internal/library.ListDirectory` provides the foundation | -| `getMusicDirectory` | Not yet implemented | same | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getMusicFolders` | 1.0.0 | ✅ | ✅ | ✅ | ✅ | ✅ | Respects per-user folder restrictions; folder IDs are small sequential integers derived from creation order (the protocol's `musicFolder.id` is `xs:int`), mapped internally onto VibeSonic's opaque folder IDs. | -## Browsing (ID3/tag-style) +## Browsing -| Endpoint | Status | Notes | -|---|---|---| -| `getArtists` | Not yet implemented | `database.ListArtists` exists | -| `getArtist` | Not yet implemented | `database.ListAlbumsByArtist` exists | -| `getAlbum` | Not yet implemented | `database.ListSongsByAlbum` exists | -| `getSong` | Not yet implemented | `database.GetSong` exists | -| `getGenres` | Not yet implemented | `database.ListGenres` exists | -| `getAlbumList` / `getAlbumList2` | Not yet implemented | | -| `getRandomSongs` | Not yet implemented | | -| `getSongsByGenre` | Not yet implemented | | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getIndexes` | 1.0.0 | ✅ | ✅ | — | ✅ | ✅ (folder restriction) | Filesystem-style browsing, driven entirely by `media_files.relative_path` — never faked from the ID3 model. | +| `getMusicDirectory` | 1.0.0 | ✅ | ✅ | — | ✅ | ✅ (folder restriction) | Directory IDs encode (folder, path); rejects a request for a folder the user can't see. | +| `getGenres` | 1.9.0 | ✅ | — | ✅ | ✅ | — | Real per-genre song/album counts. | +| `getArtists` | 1.8.0 | ✅ | — | ✅ | ✅ | — | ID3 browsing; only artists with at least one album, per protocol convention. | +| `getArtist` | 1.8.0 | ✅ | — | ✅ | ✅ | — | | +| `getAlbum` | 1.8.0 | ✅ | ✅ | ✅ | ✅ | ✅ (folder restriction on songs) | | +| `getSong` | 1.8.0 | ✅ | — | ✅ | ✅ | ✅ (folder restriction) | | +| `getVideos` | 1.8.0 | ✅ | — | ✅ | — | — | Real: lists media files the scanner tagged `kind=video`. | +| `getVideoInfo` | 1.14.0 | ⚠️ | — | — | — | — | Reports one default audio track (VibeSonic doesn't probe multi-audio-track containers yet) and real sidecar `.srt`/`.vtt`/`.ass` captions found next to the file. | +| `getArtistInfo` | 1.11.0 | ⚠️ | — | — | — | — | Honestly returns empty biography/images/similar-artists — VibeSonic has no external metadata provider in Phase 2. Not a fake success: it's an accurate "we don't have this" response. | +| `getArtistInfo2` | 1.11.0 | ⚠️ | — | — | — | — | Same as above, ID3 form. | +| `getAlbumInfo` | 1.14.0 | ⚠️ | — | — | — | — | Same reasoning as `getArtistInfo`. | +| `getAlbumInfo2` | 1.14.0 | ⚠️ | — | — | — | — | Same. | +| `getSimilarSongs` | 1.11.0 | ✅ | — | — | — | — | Real, local genre-overlap heuristic (song/album/artist seed), not fabricated data. | +| `getSimilarSongs2` | 1.11.0 | ✅ | — | — | — | — | ID3 (artist-seeded) form. | +| `getTopSongs` | 1.13.0 | ✅ | — | ✅ | — | — | Ranked by real aggregate play count across all users. | + +## Album/song lists + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getAlbumList` | 1.2.0 | ✅ | — | ✅ | ✅ | — | All 10 official types: random, newest, highest, frequent, recent, alphabeticalByName, alphabeticalByArtist, starred, byYear, byGenre. | +| `getAlbumList2` | 1.8.0 | ✅ | ✅ | ✅ | ✅ | — | Same types, ID3 form. | +| `getRandomSongs` | 1.2.0 | ✅ | — | ✅ | — | — | Supports `size`, `genre`, `fromYear`, `toYear`. | +| `getSongsByGenre` | 1.9.0 | ✅ | — | ✅ | — | — | Supports `count`/`offset`. | +| `getNowPlaying` | 1.0.0 | ✅ | — | ✅ | — | — | Real `now_playing` table, populated by `scrobble` (submission=false) and native web streaming. | +| `getStarred` | 1.8.0 | ✅ | — | ✅ | — | — | | +| `getStarred2` | 1.8.0 | ✅ | — | ✅ | — | — | | ## Search -| Endpoint | Status | -|---|---| -| `search3` | Not yet implemented | - -## Streaming / media retrieval - -| Endpoint | Status | Notes | -|---|---|---| -| `stream` | Not yet implemented | `internal/streaming` provides range-request-aware serving; the web UI's `/stream/{id}` route is the non-Subsonic equivalent used today | -| `download` | Not yet implemented | | -| `getCoverArt` | Not yet implemented | `internal/coverart` provides discovery + resize cache; the web UI's `/cover-art/{id}` route is the non-Subsonic equivalent used today | -| `getLyrics` / `getLyricsBySongId` | Not yet implemented | `lyrics`/`lyrics_lines`/`lyrics_cues` tables exist | -| `getAvatar` | Not yet implemented | | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `search` | 1.0.0 (deprecated 1.4.0) | ✅ | — | ✅ | — | — | Still fully functional, as required — deprecated doesn't mean removed. | +| `search2` | 1.4.0 | ✅ | — | ✅ | — | — | Independent offset/count per artist/album/song. | +| `search3` | 1.8.0 | ✅ | — | ✅ | ✅ | — | ID3 form; independent offset/count per category. | ## Playlists -| Endpoint | Status | Notes | -|---|---|---| -| `getPlaylists` | Not yet implemented | `database.ListPlaylistsVisibleToUser` exists; web UI has a playlists page | -| `getPlaylist` | Not yet implemented | | -| `createPlaylist` | Not yet implemented | | -| `updatePlaylist` | Not yet implemented | | -| `deletePlaylist` | Not yet implemented | | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getPlaylists` | 1.0.0 | ✅ | — | ✅ | — | — | Own + public playlists; `username` param for admins to view another user's. | +| `getPlaylist` | 1.0.0 | ✅ | — | ✅ | — | ✅ (ownership/public) | | +| `createPlaylist` | 1.0.0 | ✅ | — | ✅ | — | ✅ (playlist role) | Supports both "new playlist" and "replace an existing playlist's songs" forms. | +| `updatePlaylist` | 1.8.0 | ✅ | — | — | — | ✅ (ownership) | Rename, re-describe, publicity toggle, `songIndexToRemove`/`songIdToAdd`. | +| `deletePlaylist` | 1.0.0 | ✅ | — | — | — | ✅ (ownership) | | -## Favorites / ratings / activity +## Media retrieval -| Endpoint | Status | Notes | -|---|---|---| -| `star` / `unstar` | Not yet implemented | `database.Star`/`Unstar` exist; web UI has favorite buttons | -| `setRating` | Not yet implemented | `database.SetRating` exists | -| `scrobble` | Not yet implemented | `scrobbles` table exists | -| `getStarred` / `getStarred2` | Not yet implemented | | -| `getNowPlaying` | Not yet implemented | `now_playing` table + `database.ListNowPlaying` exist | - -## Bookmarks / play queue - -| Endpoint | Status | Notes | -|---|---|---| -| `getBookmarks` / `createBookmark` / `deleteBookmark` | Not yet implemented | `database` functions exist | -| `getPlayQueue` / `savePlayQueue` | Not yet implemented | `database` functions exist | - -## Sharing - -| Endpoint | Status | Notes | -|---|---|---| -| `getShares` / `createShare` / `updateShare` / `deleteShare` | Not yet implemented | `shares`/`share_items` tables exist | - -## Podcasts - -| Endpoint | Status | Notes | -|---|---|---| -| `getPodcasts` | Not yet implemented | `podcast_channels`/`podcast_episodes` tables exist | -| `createPodcastChannel` / `deletePodcastChannel` | Not yet implemented | | -| `downloadPodcastEpisode` / `deletePodcastEpisode` | Not yet implemented | | - -## Internet radio - -| Endpoint | Status | Notes | -|---|---|---| -| `getInternetRadioStations` | Not yet implemented | `internet_radio_stations` table exists | -| `createInternetRadioStation` / `updateInternetRadioStation` / `deleteInternetRadioStation` | Not yet implemented | | - -## Users / chat - -| Endpoint | Status | Notes | -|---|---|---| -| `getUser` / `getUsers` | Not yet implemented | native web admin has a users page | -| `createUser` / `updateUser` / `deleteUser` | Not yet implemented | | -| `addChatMessage` / `getChatMessages` | Not yet implemented | `chat_messages` table exists | +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `stream` | 1.0.0 | ✅ | n/a (binary) | n/a | ✅ | ✅ (stream role) | Direct streaming (range requests, no buffering) or real ffmpeg transcoding depending on `format`/`maxBitRate`; folder-permission checked. | +| `download` | 1.0.0 | ✅ | n/a | n/a | — | ✅ (download role) | Original bytes with `Content-Disposition: attachment`. | +| `hls` | 1.8.0 | ✅ | n/a | n/a | — | ✅ (stream role) | Real ffmpeg HLS segmenting, cached per (source, bitrate); segment URLs re-embed the caller's auth params. | +| `getCaptions` | 1.14.0 | ✅ | n/a | n/a | — | — | Serves real sidecar subtitle files. | +| `getCoverArt` | 1.0.0 | ✅ | n/a | n/a | — | ✅ (coverArt role) | Embedded or folder art, resized and cached on disk. | +| `getLyrics` | 1.2.0 | ✅ | ✅ | — | — | — | Real embedded lyrics extracted at scan time; empty (not fabricated) if none found. | +| `getAvatar` | 1.8.0 | ⚠️ | — | — | — | — | Honestly 404s — VibeSonic has no user-avatar feature in Phase 1/2. | ## Transcoding -| Endpoint | Status | Notes | -|---|---|---| -| Transcoding profiles (`internal/transcoding`) | Not yet implemented | `transcode_profiles` table exists; Phase 1 only supports direct (non-transcoded) streaming | +Integrated into `stream`/`download`/`hls` above rather than a separate +endpoint (the classic API has none). `internal/transcoding` runs real +ffmpeg processes, bounded to 4 concurrent jobs, piping output directly to +the response (never buffering a full file), and is killed when the +client disconnects (context cancellation). Supported output formats: +`mp3`, `opus`, `aac`. `maxBitRate`, `format`, and `timeOffset` are all +honored. + +## Annotation + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `star` | 1.4.0 | ✅ | ✅ | — | — | — | Song/album/artist, multiple IDs per call. | +| `unstar` | 1.4.0 | ✅ | ✅ | — | — | — | | +| `setRating` | 1.6.0 | ✅ | ✅ | — | — | — | Auto-detects song/album/artist from the given ID. | +| `scrobble` | 1.5.0 | ✅ | ✅ | — | — | — | `submission=true` records playback history + a scrobble row; `false` only updates now-playing. | + +## Sharing + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getShares` | 1.6.0 | ✅ | — | ✅ | — | ✅ (share role) | Own shares, or all shares for an admin. | +| `createShare` | 1.6.0 | ✅ | — | ✅ | — | ✅ (share role) | Songs and/or whole albums; real public URL. | +| `updateShare` | 1.6.0 | ✅ | — | — | — | ✅ (ownership) | | +| `deleteShare` | 1.6.0 | ✅ | — | — | — | ✅ (ownership) | | + +A real public, unauthenticated share page/stream route also exists at +`/share/{token}` and `/share/{token}/stream/{songID}` — see +[Web UI](#web-ui). Streaming through a share is still path-validated the +same way every other stream is; a share can never expose an arbitrary +filesystem path. + +## Podcasts + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getPodcasts` | 1.6.0 | ✅ | — | ✅ | — | ✅ (podcast role) | | +| `getNewestPodcasts` | 1.13.0 | ✅ | — | ✅ | — | ✅ (podcast role) | | +| `refreshPodcasts` | 1.9.0 | ✅ | — | — | — | ✅ (admin) | Real RSS fetch + parse for every channel, in the background. | +| `createPodcastChannel` | 1.9.0 | ✅ | — | — | — | ✅ (admin) | Kicks off a real feed refresh. | +| `deletePodcastChannel` | 1.9.0 | ✅ | — | — | — | ✅ (admin) | | +| `deletePodcastEpisode` | 1.9.0 | ✅ | — | — | — | ✅ (admin) | | +| `downloadPodcastEpisode` | 1.9.0 | ✅ | — | — | — | ✅ (admin) | Real bounded background download; the result becomes an ordinary streamable song. Interrupted downloads recover to "new" on restart (`internal/podcasts.Manager.RecoverInterruptedDownloads`, run at startup). | + +`internal/podcasts` is a real subsystem: RSS/Atom parsing +(`internal/podcasts/rss.go`), episode metadata (GUID-keyed, stable +across refreshes), enclosure downloads bounded to 2 concurrent jobs, and +downloaded episodes registered as normal `media_files`/`songs` rows so +they stream through the exact same path as scanned music. + +## Jukebox + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `jukeboxControl` | 1.2.0 (current form 1.7.0) | ✅ | ✅ | ✅ | — | ✅ (jukebox role) | Every action: get, status, set, start, stop, skip, add, clear, remove, shuffle, setGain. | + +Backed by a real `mpv` process controlled over its JSON IPC socket +(`internal/jukebox`), tested against an actual `mpv` binary (playback, +pause/resume, add/remove/clear, gain, shuffle, index seeking all +verified in `internal/jukebox/jukebox_test.go`). If `mpv` isn't +installed, the jukebox is administratively unavailable and +`jukeboxControl` returns a clear error — never fake success. + +## Internet radio + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getInternetRadioStations` | 1.9.0 | ✅ | — | ✅ | — | — | | +| `createInternetRadioStation` | 1.16.0 | ✅ | ✅ | — | — | ✅ (admin) | | +| `updateInternetRadioStation` | 1.16.0 | ✅ | — | — | — | ✅ (admin) | | +| `deleteInternetRadioStation` | 1.16.0 | ✅ | ✅ | — | — | ✅ (admin) | | + +Also exposed in the web UI at `/radio` (viewing: any signed-in user; +managing: admin). + +## Chat + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getChatMessages` | 1.2.0 | ✅ | — | ✅ | — | — | Supports `since`. | +| `addChatMessage` | 1.2.0 | ✅ | — | ✅ | — | — | | + +## User management + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getUser` | 1.3.0 | ✅ | — | ✅ | — | ✅ (self or admin) | | +| `getUsers` | 1.8.0 | ✅ | — | — | — | ✅ (admin) | | +| `createUser` | 1.1.0 | ✅ | ✅ | — | — | ✅ (admin) | Full role set + `musicFolderId` restriction. | +| `updateUser` | 1.10.1 | ✅ | ✅ | ✅ | — | ✅ (admin) | Role params not present in the request are preserved, not reset to false (regression-tested). | +| `deleteUser` | 1.3.0 | ✅ | ✅ | — | — | ✅ (admin; can't delete self) | | +| `changePassword` | 1.1.0 | ✅ | ✅ | — | — | ✅ (self or admin) | Also refreshes the encrypted copy used for token auth. | + +The full Subsonic role set (adminRole, settingsRole, streamRole, +jukeboxRole, downloadRole, uploadRole, playlistRole, coverArtRole, +commentRole, podcastRole, shareRole, videoConversionRole) is stored per +user and enforced on the server side by every endpoint above — never +only hidden in the UI. Per-user music folder restriction is also +enforced server-side (`internal/subsonic/folders.go`). + +## Bookmarks + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getBookmarks` | 1.9.0 | ✅ | — | ✅ | — | — | Per-user. | +| `createBookmark` | 1.9.0 | ✅ | ✅ | — | — | — | Position (ms) + comment. | +| `deleteBookmark` | 1.9.0 | ✅ | ✅ | — | — | — | | + +## Play queue + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getPlayQueue` | 1.12.0 | ✅ | — | ✅ | — | — | Per-user, persisted. | +| `savePlayQueue` | 1.12.0 | ✅ | ✅ | ✅ | — | — | | + +## Scanner + +| Endpoint | Version | Implemented | XML Tested | JSON Tested | Auth Tested | Permission Tested | Notes | +|---|---|---|---|---|---|---|---| +| `getScanStatus` | 1.15.0 | ✅ | — | ✅ | — | — | Real status/count from `internal/scanner`, not simulated. | +| `startScan` | 1.15.0 | ✅ | — | ✅ | — | ✅ (admin) | | + +--- ## Authentication -Phase 1's `/rest/*` authentication (token-based `u`/`t`/`s` or API key) -is not yet implemented. When it is, it will authenticate independently -of the browser session/CSRF/external-auth flow used by the web UI, per -the project's design constraint that external auth must never interfere -with native Subsonic client authentication. +Both original Subsonic authentication modes are implemented in +`internal/subsonic/auth.go`: + +- **Token/salt** (`u`, `t`, `s`): `t = md5(password + salt)`. Verified + against a separate, AES-256-GCM–encrypted copy of the password + (`internal/auth/subsonic_secret.go`) kept in sync with every password + change — never against the Argon2id hash (impossible, by design) and + never by storing the password in the clear. +- **Password** (`p`): plain or `enc:`, verified against the + Argon2id hash used everywhere else in VibeSonic. + +An account created purely through external (trusted-proxy) auth has no +local password and therefore cannot authenticate to `/rest/*` until an +administrator or the user sets one — `/rest/*` is completely independent +of the browser session/CSRF/external-auth flow (see +[`docs/external-auth.md`](external-auth.md)). + +## Common parameters + +`u`, `p`, `t`, `s`, `v`, `c`, `f` (xml/json/jsonp), and `callback` are +all handled centrally in `internal/subsonic/router.go` and +`internal/subsonic/auth.go` — no handler parses or validates them +itself. `v` is checked against `ServerVersion` (currently `1.16.1`): a +client claiming a newer protocol version gets error 20; older clients +are accepted, since the protocol is additive. + +## Error codes + +All defined in `internal/subsonic/errors.go`: 0 (generic), 10 (missing +parameter), 20 (client must upgrade), 30 (server must upgrade, reserved +— VibeSonic never needs to require a newer client), 40 (wrong +credentials), 41 (token auth unsupported for this account), 50 +(unauthorized for the operation), 60 (license/trial — never returned), +70 (not found). + +## Web UI + +Newly exposed through the Bootstrap UI in Phase 2: Podcasts (`/podcasts`, +admin at `/admin/podcasts`), Internet Radio (`/radio`), Shares +(`/shares`, plus the public `/share/{token}` page), Bookmarks +(`/bookmarks`), and per-user Subsonic permission editing +(`/admin/users/{id}/permissions`). The admin Jukebox page +(`/admin/jukebox`) shows live status and emergency stop/clear; building +a jukebox playlist is done from a Subsonic client via `jukeboxControl`, +which is the actual protocol surface for that feature. diff --git a/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/index.m3u8 b/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/index.m3u8 new file mode 100644 index 0000000..7017fd6 --- /dev/null +++ b/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/index.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:1 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:1.023322, +seg00000.ts +#EXT-X-ENDLIST diff --git a/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/seg00000.ts b/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/seg00000.ts new file mode 100644 index 0000000..7ae6f6a Binary files /dev/null and b/internal/subsonic/cache/hls/hls/_tmp_TestHLSPlaylist952250728_004_Test_Artist_Test_Album_First_Song_mp3_0_aac/seg00000.ts differ