commit 86314ecea72cf207715fd2bf568cfd39d3c6243b
parent 0e6c1544957a348a76cb2e81533d7d7c33e98b18
Author: mtmn <miro@haravara.org>
Date: Sun, 19 Apr 2026 20:51:48 +0200
fix: missing doc entries
Diffstat:
4 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
@@ -70,6 +70,8 @@ npx spago run
| Field | Default | Description |
| :--- | :--- | :--- |
+| `slug` | — | URL slug (`""` for root user, `"filip"` for `/u/filip`) |
+| `name` | — | Display name for the user (defaults to slug if not provided) |
| `listenbrainzUser` | — | ListenBrainz username to sync scrobbles from |
| `lastfmUser` | — | Last.fm username to sync scrobbles from |
| `databaseFile` | `corpus.db` | Path to the user's DuckDB database file |
diff --git a/docs/architecture.md b/docs/architecture.md
@@ -17,13 +17,16 @@ The server is built with PureScript running on Node.js. It handles several core
A Single Page Application (SPA) built with [Elm](https://elm-lang.org).
- **Real-time Updates**: Periodically refreshes the scrobble list.
- **Filtering & Search**: Supports deep filtering by genre, label, or release year.
+- **Search Functionality**: Global search across tracks, artists, albums, and labels with real-time results.
+- **About Page**: Provides system information, feature list, and links to related resources.
+- **Label Integration**: Clickable labels in track listings for quick filtering.
- **Responsive UI**: Designed for both desktop and mobile viewing with a "retro-modern" aesthetic.
### Database
Corpus uses **DuckDB** for its primary data storage. Each user has their own database file.
- **Schema**:
- `scrobbles`: Stores the core listening history (timestamp, track, artist, album, MBIDs). The `listened_at` Unix timestamp is the primary key — scrobbles from ListenBrainz and Last.fm deduplicate naturally.
- - `release_metadata`: Stores enriched metadata indexed by MusicBrainz Release ID (MBID).
+ - `release_metadata`: Stores enriched metadata indexed by MusicBrainz Release ID (MBID), including genre, label, and release year.
- **Performance**: DuckDB's columnar storage allows for extremely fast analytical queries across large listening histories.
### Storage
@@ -49,6 +52,16 @@ User configuration is split into two layers:
Each user gets their own `UserContext` with an independent DuckDB connection, sync loop, and write lock (`AVar Unit`). The write lock serializes all sync transactions — if a user has both ListenBrainz and Last.fm configured, their transactions are queued rather than run concurrently. HTTP reads do not acquire the lock; DuckDB's MVCC provides consistent snapshots.
+### Graceful Shutdown
+
+The server implements graceful shutdown handling to ensure clean termination of all background processes. When the server receives a shutdown signal, it:
+
+1. **Kills background fibers**: Terminates all running background tasks (metadata enrichment, database backups) for each user
+2. **Closes database connections**: Properly closes all DuckDB connections
+3. **Logs cleanup progress**: Provides detailed logging during the shutdown process
+
+This prevents data corruption and ensures that any in-progress operations are completed or safely aborted before the server exits.
+
## Configuration Reference
### Environment Variables
@@ -74,6 +87,7 @@ Each user gets their own `UserContext` with an independent DuckDB connection, sy
| Field | Type | Purpose |
|---|---|---|
| `slug` | `Text` | URL slug (`""` for root user, `"filip"` for `/u/filip`) |
+| `name` | `Optional Text` | Display name for the user (defaults to slug if not provided) |
| `listenbrainzUser` | `Optional Text` | ListenBrainz username |
| `lastfmUser` | `Optional Text` | Last.fm username |
| `databaseFile` | `Text` | DuckDB filename (relative to `DATABASE_PATH`) |
diff --git a/docs/duckdb_queries.md b/docs/duckdb_queries.md
@@ -47,6 +47,17 @@ ORDER BY play_count DESC
LIMIT 10;
```
+### Top 10 Labels
+```sql
+SELECT rm.label, count(*) as play_count
+FROM scrobbles s
+JOIN release_metadata rm ON s.release_mbid = rm.release_mbid
+WHERE rm.label IS NOT NULL AND rm.label != ''
+GROUP BY rm.label
+ORDER BY play_count DESC
+LIMIT 10;
+```
+
## Time-based Analysis
### Scrobbles per day (Last 30 days)
diff --git a/src/Main.purs b/src/Main.purs
@@ -693,7 +693,7 @@ fetchCosineSimilar slug cfg query = do
liftEffect $ Metrics.incCosineRequest slug "not_configured"
pure "{\"data\":{\"similar_tracks\":[]},\"success\":true}"
else do
- let headers = { "User-Agent": "corpus/1.0 +https://codeberg.org/mtmn/corpus", "Authorization": "Bearer " <> apiKey }
+ let headers = { "User-Agent": "corpus/1.0 +https://github.com/mtmn/corpus", "Authorization": "Bearer " <> apiKey }
let searchUrl = "https://cosine.club/api/v1/search?q=" <> (fromMaybe "" $ encodeURIComponent query) <> "&limit=1"
Log.info $ "Cosine Club: searching for: " <> query
searchResult <- try $ fetch searchUrl { method: GET, headers: headers }
@@ -791,7 +791,7 @@ type MbData = { genre :: Maybe String, label :: Maybe String, year :: Maybe Int
fetchMusicBrainzRelease :: String -> Aff (Maybe MbData)
fetchMusicBrainzRelease mbid = do
let url = "https://musicbrainz.org/ws/2/release/" <> mbid <> "?inc=genres+labels+release-groups&fmt=json"
- result <- try $ fetch url { method: GET, headers: { "User-Agent": "corpus/1.0 +https://codeberg.org/mtmn/corpus" } }
+ result <- try $ fetch url { method: GET, headers: { "User-Agent": "corpus/1.0 +https://github.com/mtmn/corpus" } }
case result of
Left err -> do
Log.error $ "MusicBrainz fetch error for " <> mbid <> ": " <> Exception.message err