corpus

Log | Files | Refs | README | LICENSE

commit 86314ecea72cf207715fd2bf568cfd39d3c6243b
parent 0e6c1544957a348a76cb2e81533d7d7c33e98b18
Author: mtmn <miro@haravara.org>
Date:   Sun, 19 Apr 2026 20:51:48 +0200

fix: missing doc entries

Diffstat:
MREADME.md | 2++
Mdocs/architecture.md | 16+++++++++++++++-
Mdocs/duckdb_queries.md | 11+++++++++++
Msrc/Main.purs | 4++--
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