# 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).