Files
vibesonic/docs/architecture.md
T

5.3 KiB

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