Add Docker/systemd deployment, documentation, extauth logout URL wiring
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
FROM golang:1.25-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
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 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& useradd -r -u 10001 -d /data -s /usr/sbin/nologin vibesonic \
|
||||||
|
&& mkdir -p /data /music \
|
||||||
|
&& chown -R vibesonic:vibesonic /data
|
||||||
|
|
||||||
|
COPY --from=build /out/vibesonic /usr/local/bin/vibesonic
|
||||||
|
|
||||||
|
USER vibesonic
|
||||||
|
VOLUME ["/data", "/music"]
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENV VIBESONIC_LISTEN_ADDR=:8080 \
|
||||||
|
VIBESONIC_DATA_DIR=/data \
|
||||||
|
VIBESONIC_DB=/data/vibesonic.db \
|
||||||
|
VIBESONIC_MUSIC_DIRS=/music \
|
||||||
|
VIBESONIC_FFMPEG_PATH=/usr/bin/ffmpeg \
|
||||||
|
VIBESONIC_FFPROBE_PATH=/usr/bin/ffprobe \
|
||||||
|
VIBESONIC_SCAN_INTERVAL=1h
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/vibesonic"]
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# VibeSonic
|
||||||
|
|
||||||
|
VibeSonic is a lightweight, self-hosted music/media server written in Go.
|
||||||
|
It follows the same philosophy as other small `Vibe*` tools: a single Go
|
||||||
|
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
|
||||||
|
[`docs/subsonic-compatibility.md`](docs/subsonic-compatibility.md) for
|
||||||
|
what's planned.
|
||||||
|
|
||||||
|
## Features (Phase 1)
|
||||||
|
|
||||||
|
- SQLite storage (WAL mode, foreign keys, versioned migrations) with a
|
||||||
|
schema already modeling artists, albums, songs, genres, contributors,
|
||||||
|
playlists, favorites, ratings, scrobbles, playback history, bookmarks,
|
||||||
|
play queues, shares, podcasts, internet radio, and settings
|
||||||
|
- Native authentication: Argon2id password hashing, HTTP-only signed
|
||||||
|
session cookies, CSRF-protected forms, session revocation
|
||||||
|
- Optional Navidrome-style external authentication via a trusted reverse
|
||||||
|
proxy header, gated on the real TCP peer address (never on
|
||||||
|
spoofable `X-Forwarded-For`/`X-Real-IP`/`Forwarded` headers)
|
||||||
|
- A library scanner supporting multiple music folders, incremental
|
||||||
|
rescans, new/changed/deleted/renamed file detection, and a bounded
|
||||||
|
worker pool — never a blind full rebuild
|
||||||
|
- Real metadata extraction via `ffprobe` (multi-artist, multi-genre,
|
||||||
|
MusicBrainz IDs, ReplayGain, embedded artwork/lyrics, sort fields, and
|
||||||
|
more)
|
||||||
|
- Stable, opaque, URL-safe IDs that survive restarts and rescans
|
||||||
|
- Direct media streaming with HTTP range request support, correct MIME
|
||||||
|
types, and no full-file buffering
|
||||||
|
- Cover art discovery (embedded artwork or folder images) with a
|
||||||
|
resized-image disk cache
|
||||||
|
- A server-rendered, dark-mode, keyboard-friendly web UI: browsing,
|
||||||
|
search, playlists, favorites, a persistent bottom player, and an admin
|
||||||
|
section (users, music folders, scanner status, system settings)
|
||||||
|
- `/healthz` and `/readyz`
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```
|
||||||
|
go build -o vibesonic ./cmd/vibesonic
|
||||||
|
VIBESONIC_SESSION_SECRET=$(openssl rand -hex 32) ./vibesonic
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:8080` — on a fresh database you'll land on
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`docker-compose.yml`](docker-compose.yml) — mount your music
|
||||||
|
directory read-only at `/music` and set `VIBESONIC_SESSION_SECRET`.
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
|
||||||
|
See [`systemd/vibesonic.service`](systemd/vibesonic.service).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
VibeSonic is configured entirely through `VIBESONIC_*` environment
|
||||||
|
variables — see [`docs/configuration.md`](docs/configuration.md) for the
|
||||||
|
full list and defaults. There is no separate config file.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [`docs/architecture.md`](docs/architecture.md) — how the pieces fit together
|
||||||
|
- [`docs/configuration.md`](docs/configuration.md) — every environment variable
|
||||||
|
- [`docs/external-auth.md`](docs/external-auth.md) — trusted reverse-proxy authentication
|
||||||
|
- [`docs/subsonic-compatibility.md`](docs/subsonic-compatibility.md) — protocol endpoint status
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```
|
||||||
|
go fmt ./...
|
||||||
|
go vet ./...
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Most tests exercise real SQLite databases and, where relevant, the real
|
||||||
|
`ffmpeg`/`ffprobe` binaries (skipped automatically if not installed).
|
||||||
|
No mocks are used for the database.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
vibesonic:
|
||||||
|
build: .
|
||||||
|
image: vibesonic:latest
|
||||||
|
container_name: vibesonic
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- vibesonic-data:/data
|
||||||
|
- /path/to/your/music:/music:ro
|
||||||
|
environment:
|
||||||
|
VIBESONIC_SESSION_SECRET: "change-me-to-a-random-string"
|
||||||
|
# VIBESONIC_SCAN_INTERVAL: 1h
|
||||||
|
# VIBESONIC_EXTAUTH_ENABLED: "false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vibesonic-data:
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
VibeSonic is a single Go binary. There are no separate network services:
|
||||||
|
the web UI, (in later phases) the Subsonic API, the scanner, and
|
||||||
|
streaming all run in one process against one SQLite database.
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser -----------+
|
||||||
|
|
|
||||||
|
Subsonic clients ---+--> VibeSonic (single process)
|
||||||
|
(Phase 2+) | |
|
||||||
|
| +-- HTTP/Web (internal/web)
|
||||||
|
| +-- Authentication (internal/auth)
|
||||||
|
| +-- Library (internal/library)
|
||||||
|
| +-- Scanner (internal/scanner)
|
||||||
|
| +-- Streaming (internal/streaming)
|
||||||
|
| +-- Metadata (internal/metadata)
|
||||||
|
| +-- Cover art (internal/coverart)
|
||||||
|
| +-- SQLite (internal/database)
|
||||||
|
|
|
||||||
|
+--> Local music folders
|
||||||
|
|
||||||
|
ffmpeg/ffprobe (external processes, invoked with
|
||||||
|
explicit argument lists — never a shell)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/vibesonic/ entry point: loads config, opens the app, runs it
|
||||||
|
internal/
|
||||||
|
app/ wires every subsystem together; owns the HTTP
|
||||||
|
server lifecycle and background loops
|
||||||
|
auth/ password hashing, session cookies, CSRF,
|
||||||
|
external (trusted-proxy) authentication
|
||||||
|
config/ VIBESONIC_* environment variable loading
|
||||||
|
coverart/ cover art discovery + resized-image disk cache
|
||||||
|
database/ SQLite connection, migrations, and one query
|
||||||
|
file per area of the schema (users, sessions,
|
||||||
|
library, activity, ...)
|
||||||
|
idgen/ stable opaque ID generation
|
||||||
|
library/ turns parsed tags into the artist/album/song
|
||||||
|
relational model; filesystem-style directory
|
||||||
|
listing
|
||||||
|
metadata/ ffprobe-based tag/technical-property extraction
|
||||||
|
scanner/ walks music folders, detects new/changed/
|
||||||
|
deleted/renamed files, indexes them
|
||||||
|
streaming/ range-request-aware file serving, path safety
|
||||||
|
web/ server-rendered Bootstrap 5 UI (handlers,
|
||||||
|
templates via the top-level web/ package)
|
||||||
|
migrations/ embedded SQL schema migrations
|
||||||
|
web/ embedded templates + static assets (Bootstrap,
|
||||||
|
icons, player JS/CSS)
|
||||||
|
```
|
||||||
|
|
||||||
|
`internal/database` is the only package that talks to SQLite directly.
|
||||||
|
Every other package calls into it rather than holding its own
|
||||||
|
`*sql.DB` query logic, so the schema and its access patterns stay in one
|
||||||
|
place.
|
||||||
|
|
||||||
|
## Data flow: scanning
|
||||||
|
|
||||||
|
1. `internal/scanner` walks each enabled music folder's filesystem tree
|
||||||
|
with a bounded worker pool (4 concurrent files).
|
||||||
|
2. For each recognized audio file, it compares size+mtime against the
|
||||||
|
`media_files` row for that (folder, relative path) natural key:
|
||||||
|
- Unchanged → just marked seen (`last_scanned_at` touched), no reprobe.
|
||||||
|
- Missing by path but a `media_files` row with the same content
|
||||||
|
fingerprint exists elsewhere in the folder → treated as a
|
||||||
|
rename/move; the existing row's path is updated in place.
|
||||||
|
- New or changed → probed via `internal/metadata` (ffprobe), then
|
||||||
|
`internal/library.IndexFile` creates/updates the artist, album,
|
||||||
|
song, genre, and credit rows, reusing existing rows by natural key
|
||||||
|
(name, title+artist, etc.) so IDs stay stable.
|
||||||
|
3. After the walk, any file not touched during this scan pass is marked
|
||||||
|
`missing` (not deleted) — playlists, favorites, and play history
|
||||||
|
referencing it survive if the file reappears later.
|
||||||
|
|
||||||
|
## Data flow: streaming
|
||||||
|
|
||||||
|
1. The web handler resolves a song ID to its `media_files` row and owning
|
||||||
|
`music_folders` row.
|
||||||
|
2. `internal/streaming.ResolvePath` re-joins the folder root with the
|
||||||
|
stored relative path and verifies (after resolving symlinks) that the
|
||||||
|
result is still inside the folder root, rejecting any traversal.
|
||||||
|
3. `internal/streaming.ServeFile` opens the file and hands it to
|
||||||
|
`http.ServeContent`, which streams directly from disk (no full-file
|
||||||
|
buffering) and handles HTTP range requests and conditional GETs.
|
||||||
|
|
||||||
|
Authorization (is this caller allowed to stream anything at all) happens
|
||||||
|
in the router, before any of the above runs: the route is wrapped in
|
||||||
|
`auth.Middleware.RequireUser`.
|
||||||
|
|
||||||
|
## Stable IDs
|
||||||
|
|
||||||
|
See [`internal/idgen`](../internal/idgen/idgen.go). IDs are random,
|
||||||
|
prefixed by type (`sg_` songs, `al_` albums, `ar_` artists, ...), and
|
||||||
|
assigned exactly once per row. Rescans never regenerate them: the
|
||||||
|
scanner and library indexer always look up existing rows by natural key
|
||||||
|
first (file path within a folder; artist name + MusicBrainz ID; album
|
||||||
|
title + primary artist; ...) and only call `idgen.New` for rows that have
|
||||||
|
never been seen before.
|
||||||
|
|
||||||
|
## Why no ORM / interfaces-for-purity
|
||||||
|
|
||||||
|
`database/sql` with explicit SQL and hand-scanned structs, one query file
|
||||||
|
per schema area. No interfaces are introduced purely for testability or
|
||||||
|
abstraction — tests use a real temporary SQLite database (see
|
||||||
|
`internal/database/database_test.go` and friends), because SQLite is
|
||||||
|
fast enough that this adds no meaningful cost and keeps the tests honest
|
||||||
|
about actual query behavior (foreign keys, constraints, collation).
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
VibeSonic is configured entirely through environment variables. There is
|
||||||
|
no separate config file — everything needed to run the server is
|
||||||
|
available via `VIBESONIC_*` variables, with sane defaults for everything
|
||||||
|
except the session secret.
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `VIBESONIC_LISTEN_ADDR` | `:8080` | HTTP listen address |
|
||||||
|
| `VIBESONIC_DATA_DIR` | `/var/lib/vibesonic` | Directory for the database, resized cover-art cache, etc. |
|
||||||
|
| `VIBESONIC_DB` | `<DATA_DIR>/vibesonic.db` | Path to the SQLite database file |
|
||||||
|
| `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_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. |
|
||||||
|
|
||||||
|
## External authentication
|
||||||
|
|
||||||
|
See [`docs/external-auth.md`](external-auth.md) for the full
|
||||||
|
`VIBESONIC_EXTAUTH_*` reference.
|
||||||
|
|
||||||
|
## First run
|
||||||
|
|
||||||
|
On a database with no users yet, every request redirects to `/setup`,
|
||||||
|
which walks you through creating the administrator account and your
|
||||||
|
first music folder, detects `ffmpeg`/`ffprobe`, and kicks off the first
|
||||||
|
scan. VibeSonic never ships default credentials — the administrator
|
||||||
|
account only exists once you create it through this wizard.
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Schema migrations are embedded in the binary (`migrations/*.sql`) and
|
||||||
|
applied automatically on startup, tracked in a `schema_migrations` table.
|
||||||
|
There is nothing to run manually.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# External authentication
|
||||||
|
|
||||||
|
VibeSonic supports optional Navidrome-style externalized authentication:
|
||||||
|
a trusted reverse proxy authenticates the browser user (via whatever
|
||||||
|
mechanism it likes — OIDC, SAML, HTTP Basic Auth, a VPN identity, ...)
|
||||||
|
and asserts that identity to VibeSonic via an HTTP header. **VibeSonic
|
||||||
|
never implements OIDC or talks to an identity provider itself** — the
|
||||||
|
proxy handles authentication, VibeSonic handles users and authorization.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `VIBESONIC_EXTAUTH_ENABLED` | `false` | Enable external authentication |
|
||||||
|
| `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` | *(none)* | Comma-separated list of IPv4/IPv6 CIDRs (or bare IPs) that are trusted to assert identity, e.g. `127.0.0.1/32,10.0.0.5/32` |
|
||||||
|
| `VIBESONIC_EXTAUTH_USER_HEADER` | `Remote-User` | Header name carrying the authenticated username |
|
||||||
|
| `VIBESONIC_EXTAUTH_LOGOUT_URL` | *(none)* | Optional URL to send users to on logout (e.g. the proxy's own logout endpoint), instead of VibeSonic's local `/login` |
|
||||||
|
| `VIBESONIC_EXTAUTH_AUTO_CREATE_USERS` | `true` | Automatically create a VibeSonic user the first time a trusted identity is seen |
|
||||||
|
|
||||||
|
## The critical security property
|
||||||
|
|
||||||
|
**The identity header is only trusted when the actual TCP connection
|
||||||
|
peer address matches a configured trusted CIDR.** This check
|
||||||
|
(`internal/auth/extauth.go`) reads `http.Request.RemoteAddr` — the real
|
||||||
|
socket peer — never a header.
|
||||||
|
|
||||||
|
Headers like `X-Forwarded-For`, `X-Real-IP`, or `Forwarded` are
|
||||||
|
**never** consulted to decide whether the peer is trusted. Those headers
|
||||||
|
are attacker-controlled: any client can set them to anything. If
|
||||||
|
VibeSonic trusted them, an arbitrary internet client could add
|
||||||
|
`X-Forwarded-For: 127.0.0.1` and a `Remote-User: admin` header to its own
|
||||||
|
direct request and be treated as an authenticated administrator.
|
||||||
|
|
||||||
|
This is why `VIBESONIC_EXTAUTH_TRUSTED_SOURCES` has **no default** — not
|
||||||
|
`0.0.0.0/0`, not `::/0`, not RFC1918 ranges. External auth trusts nothing
|
||||||
|
until you explicitly name a network (typically `127.0.0.1/32` for a
|
||||||
|
proxy on the same host, or the proxy's specific address on a Docker
|
||||||
|
bridge network).
|
||||||
|
|
||||||
|
See `internal/auth/extauth_test.go` for the regression tests covering
|
||||||
|
this, including a test that a spoofed `X-Forwarded-For`/`X-Real-IP`/
|
||||||
|
`Forwarded` header from an untrusted peer is ignored entirely.
|
||||||
|
|
||||||
|
## How it resolves to a user
|
||||||
|
|
||||||
|
A trusted identity resolves to an ordinary VibeSonic user (with
|
||||||
|
`external_auth = 1`), never a separate "external user" type. If
|
||||||
|
`VIBESONIC_EXTAUTH_AUTO_CREATE_USERS=true` and no user with that username
|
||||||
|
exists yet, one is created with no local password. If a user with that
|
||||||
|
username already exists (created locally or by a previous external
|
||||||
|
login), it's reused.
|
||||||
|
|
||||||
|
## Interaction with native auth
|
||||||
|
|
||||||
|
External auth and native session/password login coexist: the
|
||||||
|
`Authenticate` middleware tries the session cookie first, then falls back
|
||||||
|
to the trusted-proxy header. A user can still log in with a local
|
||||||
|
password on VibeSonic's own `/login` page even when external auth is
|
||||||
|
enabled, unless their account has no password hash set (auto-created
|
||||||
|
external users start this way).
|
||||||
|
|
||||||
|
In later phases, `/rest/*` (the native Subsonic API) will authenticate
|
||||||
|
exclusively via Subsonic's own token/API-key scheme and must never be
|
||||||
|
affected by external auth's browser-cookie flow.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Subsonic / OpenSubsonic 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.
|
||||||
|
|
||||||
|
This matrix will be filled in as `internal/subsonic` and
|
||||||
|
`internal/opensubsonic` are implemented. For now, every endpoint is
|
||||||
|
"Not yet implemented."
|
||||||
|
|
||||||
|
## System
|
||||||
|
|
||||||
|
| Endpoint | Status |
|
||||||
|
|---|---|
|
||||||
|
| `ping` | Not yet implemented |
|
||||||
|
| `getLicense` | Not yet implemented |
|
||||||
|
|
||||||
|
## Browsing (filesystem-style)
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
## Browsing (ID3/tag-style)
|
||||||
|
|
||||||
|
| 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 | |
|
||||||
|
|
||||||
|
## 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 | |
|
||||||
|
|
||||||
|
## 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 | |
|
||||||
|
|
||||||
|
## Favorites / ratings / activity
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
## Transcoding
|
||||||
|
|
||||||
|
| Endpoint | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Transcoding profiles (`internal/transcoding`) | Not yet implemented | `transcode_profiles` table exists; Phase 1 only supports direct (non-transcoded) streaming |
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -65,7 +65,11 @@ func (h *Web) loginFailed(w http.ResponseWriter, r *http.Request, next string) {
|
|||||||
|
|
||||||
func (h *Web) handleLogout(w http.ResponseWriter, r *http.Request) {
|
func (h *Web) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
h.Sessions.Clear(w, r)
|
h.Sessions.Clear(w, r)
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
dest := "/login"
|
||||||
|
if h.Config.ExtAuth.Enabled && h.Config.ExtAuth.LogoutURL != "" {
|
||||||
|
dest = h.Config.ExtAuth.LogoutURL
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, dest, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- First-run setup ---
|
// --- First-run setup ---
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=VibeSonic - lightweight self-hosted music server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
Documentation=https://git.noroutes.net/NoRoutes/vibetunes
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=vibesonic
|
||||||
|
Group=vibesonic
|
||||||
|
Environment=VIBESONIC_LISTEN_ADDR=:8080
|
||||||
|
Environment=VIBESONIC_DATA_DIR=/var/lib/vibesonic
|
||||||
|
Environment=VIBESONIC_DB=/var/lib/vibesonic/vibesonic.db
|
||||||
|
Environment=VIBESONIC_MUSIC_DIRS=/music
|
||||||
|
Environment=VIBESONIC_FFMPEG_PATH=/usr/bin/ffmpeg
|
||||||
|
Environment=VIBESONIC_FFPROBE_PATH=/usr/bin/ffprobe
|
||||||
|
Environment=VIBESONIC_SCAN_INTERVAL=1h
|
||||||
|
# Generate a real secret: openssl rand -hex 32
|
||||||
|
Environment=VIBESONIC_SESSION_SECRET=change-me-to-a-random-string
|
||||||
|
ExecStart=/usr/local/bin/vibesonic
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2s
|
||||||
|
TimeoutStopSec=30s
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
LockPersonality=true
|
||||||
|
ReadOnlyPaths=/music
|
||||||
|
ReadWritePaths=/var/lib/vibesonic
|
||||||
|
|
||||||
|
StateDirectory=vibesonic
|
||||||
|
StateDirectoryMode=0700
|
||||||
|
|
||||||
|
LimitNOFILE=65535
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user