- Go 52.8%
- JavaScript 33.7%
- CSS 10.9%
- HTML 2.6%
| db | ||
| indexer | ||
| static | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
Status
- Songs can be played.
- Artists: "Play all songs of this artist" works.
- Mixes: A separate directory where you can have a DJ's directory containing their mixes.
Music Player — Project Spec
A personal, self-hosted music player web app. Music files (primarily MP3) are stored on disk organized as /musicdir/Artist/Album/Song.mp3. A SQLite database holds song metadata (parsed from ID3 tags) and playlist definitions, which can be either auto-generated from metadata or manually curated. An indexer/scanner component keeps the database in sync with the filesystem.
The backend is written in Go using the standard library HTTP server, and handles API endpoints for metadata queries, search, and playlist logic. It does not serve music files directly. Music files are served by Caddy (which handles range requests, partial content, and caching automatically), with the music directory mounted at /music/*. The Go API returns /music/... URLs for each song, which the frontend passes directly to an HTML element for playback. The entire stack sits behind an Authelia instance — no auth logic lives in the app itself.
The guiding principle throughout is simplicity: sqlite over postgres, over a JS audio library, Caddy for file serving over reimplementing it in Go, filesystem as the source of truth for audio data.
How to build
This project uses modernc.org/sqlite, a Go-native SQLite driver. No C compiler or CGO setup is needed.
go build .
Or run directly:
go run . --music ./music --db ./music.db
API Reference
All endpoints return JSON. Errors use the shape { "error": "message" } with an appropriate HTTP status code.
Songs
-
GET /api/songs— List all songs, paginated.
Query params:limit(default 500, capped at 1000),offset.
Returns an array ofSongobjects ordered by artist, album, track. -
GET /api/songs/{id}— Get a single song by its database ID.
Returns aSongobject or404. -
GET /api/search?q={query}— Full-text search via SQLite FTS5 across title, artist, album, and genre.
Returns up to 200Songobjects ranked by relevance.
Artists & Albums
-
GET /api/artists— List every distinct artist in the library.
Returns["Artist A", "Artist B", ...]. -
GET /api/artists/{artist}/albums— List albums for a given artist, ordered by year then album title.
Returns["Album A", "Album B", ...]. -
GET /api/artists/{artist}/albums/{album}/songs— List songs on a specific album.
Returns an array ofSongobjects ordered by track number.
Playlists
-
GET /api/playlists— List all playlists.
Returns an array ofPlaylistobjects ordered by name. -
POST /api/playlists— Create a new playlist.
Body:{ "name": "...", "description": "...", "type": "manual", "query": "" }
Returns the createdPlaylistwith its generatedid(201 Created). -
GET /api/playlists/{id}— Get playlist metadata. Returns aPlaylistobject or404. -
PUT /api/playlists/{id}— Update playlist metadata.
Body: same as create. Returns the updatedPlaylist. -
DELETE /api/playlists/{id}— Delete a playlist. Returns204 No Content. -
GET /api/playlists/{id}/songs— Get the songs inside a playlist.
For query playlists the result is resolved dynamically fromquery; for manual and directory playlists the stored song order is returned.
Returns an array ofSongobjects. -
POST /api/playlists/{id}/songs— Add a song to a manual playlist.
Body:{ "songId": 123 }. Returns201 Createdwith{ "status": "added" }. -
DELETE /api/playlists/{id}/songs/{songId}— Remove a song from a playlist. Returns204 No Content.
Scan & Stats
-
POST /api/scan— Trigger a manual filesystem scan in the background.
Returns202 Acceptedwith{ "status": "scanning" }. -
GET /api/stats— Quick library counts.
Returns{ "total": 1234, "artists": 42, "albums": 67 }.
Static Files
GET /— The Go server serves the frontend static files (index.html,app.js,style.css) from thestatic/directory.
Audio File Serving
Music files are NOT served by the Go backend.
MP3s are exposed as plain static files by Caddy (or another reverse proxy) mounted at/music/*. The API returns songpathvalues such as/music/Artist/Album/Song.mp3, which the frontend passes directly to an<audio>element. Caddy handles range requests, partial content, and caching automatically.
Response Shapes
Song
{
"id": 1,
"title": "Song Title",
"artist": "Artist Name",
"album": "Album Name",
"year": 2020,
"track": 1,
"genre": "Rock",
"path": "/music/Artist/Album/Song.mp3",
"duration": 180
}
Playlist
{
"id": 1,
"name": "My Playlist",
"description": "A cool playlist",
"type": "manual",
"query": "artist:Radiohead album:OK Computer",
"editable": true
}
type is one of:
"manual"— user-curated via the API (editable: true)."query"— auto-generated from a metadata query (editable: false)."directory"— auto-generated from subdirectories ofplaylists/in the music library (editable: false).
Directory playlists are recreated from scratch on every library scan. Each direct subdirectory of playlists/ becomes a playlist (subdirectory name = playlist name), and the audio files inside it become the tracks, ordered alphabetically by filename. Files not present in the song index are skipped.
Query-playlist query supports a small grammar: artist:X, album:Y, year:Z, genre:G, or free text which hits the FTS5 index.