Add Docker/systemd deployment, documentation, extauth logout URL wiring
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user