commit 030a32b4d7a9bd7b8fee3367d3a1d92daeb5dd98
parent 28b040e16e8438368723dd6a8aeac5457317aee9
Author: mtmn <miro@haravara.org>
Date: Sat, 11 Apr 2026 17:09:55 +0200
docs: readme
Diffstat:
5 files changed, 42 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
@@ -2,29 +2,5 @@
| | |
| :--- | ---: |
-| Scorpus is a ListenBrainz frontend built with PureScript that provides server-side rendering and client-side interactivity using Halogen. It enables music listening tracking with DuckDB/SQLite storage and S3 integration for data management. | <img src="docs/korpus.webp" width="400" alt="Korpus"> |
+| Scorpus is a [ListenBrainz](https://listenbrainz.org) frontend built with PureScript and Halogen. Under the hood it stores track metadata in DuckDB and cover images in a S3 bucket. | <img src="docs/korpus.webp" width="400" alt="Korpus"> |
----
-
-## Architecture
-
-### Client-Side (Browser)
-
-- **Halogen** — Component-based UI framework for declarative, reactive interfaces
-- **Affjax / Fetch** — HTTP client for API communication with the backend
-- **Argonaut** — JSON encoding/decoding for structured data exchange
-- **Web HTML / DOM** — Low-level browser API bindings for direct DOM manipulation
-
-### Server-Side (Node.js)
-
-- **Node HTTP** — Built-in Node.js HTTP server for request handling
-- **DuckDB / SQLite3** — Embedded analytical and transactional databases for music listening data
-- **AWS S3 SDK** — Object storage integration for large-scale data export/import
-- **Environment Config** — Dotenv-based configuration management
-
-### Shared
-
-- **PureScript 0.15** — Strongly-typed functional language compiled to JavaScript
-- **Esbuild** — Fast bundler producing both server (Node ESM) and client (browser) bundles
-- **Spago** — PureScript package manager and build tool
-- **Argonaut Core** — Shared JSON serialization layer across client and server
diff --git a/src/Client.purs b/src/Client.purs
@@ -246,12 +246,17 @@ component =
]
]
]
- , if Set.member coverUrl failedCovers then HH.text ""
- else HH.img
- [ HP.class_ (H.ClassName "track-cover")
- , HP.src coverUrl
- , HP.alt release
- , HE.onError \_ -> ImageError coverUrl
+ , HH.div [ HP.class_ (H.ClassName "cover-wrapper") ]
+ [ if Set.member coverUrl failedCovers then HH.text ""
+ else HH.img
+ [ HP.class_ (H.ClassName "track-cover")
+ , HP.src coverUrl
+ , HP.alt release
+ , HE.onError \_ -> ImageError coverUrl
+ ]
+ , case track.genre of
+ Just g -> HH.div [ HP.class_ (H.ClassName "genre-tag") ] [ HH.text g ]
+ Nothing -> HH.text ""
]
]
diff --git a/src/Db.purs b/src/Db.purs
@@ -82,7 +82,8 @@ upsertScrobble conn (Listen { listenedAt, trackMetadata: TrackMetadata track })
getScrobbles :: Connection -> Int -> Int -> Maybe { field :: String, value :: String } -> Aff (Array Listen)
getScrobbles conn limit offset Nothing = do
- rows <- queryAll conn "SELECT listened_at, track_name, artist_name, release_name, release_mbid, caa_release_mbid FROM scrobbles ORDER BY listened_at DESC LIMIT ? OFFSET ?"
+ rows <- queryAll conn
+ "SELECT s.listened_at, s.track_name, s.artist_name, s.release_name, s.release_mbid, s.caa_release_mbid, rm.genre FROM scrobbles s LEFT JOIN release_metadata rm ON s.release_mbid = rm.release_mbid ORDER BY s.listened_at DESC LIMIT ? OFFSET ?"
[ unsafeCoerce limit, unsafeCoerce offset ]
pure $ fromMaybe [] $ traverse rowToListen rows
getScrobbles conn limit offset (Just { field, value }) = do
@@ -92,7 +93,7 @@ getScrobbles conn limit offset (Just { field, value }) = do
"year" -> "rm.release_year::VARCHAR"
_ -> "rm.genre"
rows <- queryAll conn
- ( "SELECT s.listened_at, s.track_name, s.artist_name, s.release_name, s.release_mbid, s.caa_release_mbid"
+ ( "SELECT s.listened_at, s.track_name, s.artist_name, s.release_name, s.release_mbid, s.caa_release_mbid, rm.genre"
<> " FROM scrobbles s JOIN release_metadata rm ON s.release_mbid = rm.release_mbid"
<> " WHERE "
<> col
@@ -154,6 +155,7 @@ rowToListen json = do
releaseName <- Object.lookup "release_name" obj >>= toString
releaseMbid <- Object.lookup "release_mbid" obj >>= toString
caaReleaseMbid <- Object.lookup "caa_release_mbid" obj >>= toString
+ let genre = Object.lookup "genre" obj >>= toString
pure $ Listen
{ listenedAt: Just listenedAt
@@ -161,6 +163,7 @@ rowToListen json = do
{ trackName: Just trackName
, artistName: Just artistName
, releaseName: Just releaseName
+ , genre
, mbidMapping: Just $ MbidMapping
{ releaseMbid: if releaseMbid == "" then Nothing else Just releaseMbid
, caaReleaseMbid: if caaReleaseMbid == "" then Nothing else Just caaReleaseMbid
diff --git a/src/Main.purs b/src/Main.purs
@@ -258,7 +258,6 @@ indexHtml =
.track-cover {
width: 60px;
height: 60px;
- margin-left: 15px;
border-radius: 4px;
object-fit: cover;
background: rgba(255, 255, 255, 0.05);
@@ -443,6 +442,26 @@ indexHtml =
padding: 10px 0;
}
+ .cover-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-left: 15px;
+ gap: 4px;
+ flex-shrink: 0;
+ }
+
+ .genre-tag {
+ font-size: 10px;
+ color: #9fbfe7;
+ text-align: center;
+ max-width: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ opacity: 0.8;
+ }
+
.stat-row {
cursor: pointer;
}
diff --git a/src/Types.purs b/src/Types.purs
@@ -69,6 +69,7 @@ newtype TrackMetadata = TrackMetadata
, artistName :: Maybe String
, releaseName :: Maybe String
, mbidMapping :: Maybe MbidMapping
+ , genre :: Maybe String
}
derive instance eqTrackMetadata :: Eq TrackMetadata
@@ -81,14 +82,16 @@ instance DecodeJson TrackMetadata where
artistName <- obj .:? "artist_name"
releaseName <- obj .:? "release_name"
mbidMapping <- obj .:? "mbid_mapping"
- pure $ TrackMetadata { trackName, artistName, releaseName, mbidMapping }
+ genre <- obj .:? "genre"
+ pure $ TrackMetadata { trackName, artistName, releaseName, mbidMapping, genre }
instance EncodeJson TrackMetadata where
- encodeJson (TrackMetadata { trackName, artistName, releaseName, mbidMapping }) =
+ encodeJson (TrackMetadata { trackName, artistName, releaseName, mbidMapping, genre }) =
"track_name" := encodeJson trackName
~> "artist_name" := encodeJson artistName
~> "release_name" := encodeJson releaseName
~> "mbid_mapping" := encodeJson mbidMapping
+ ~> "genre" := encodeJson genre
~> jsonEmptyObject
newtype MbidMapping = MbidMapping