commit ac540524df7d2b50eac6ba64abd1943ebff6ed3c
parent e0b2708337cc3ef8f418dc3f2098ee13fd133ba1
Author: mtmn <miro@haravara.org>
Date: Sat, 18 Apr 2026 22:39:05 +0200
chore: linter fixes
Diffstat:
4 files changed, 82 insertions(+), 73 deletions(-)
diff --git a/src/Client.elm b/src/Client.elm
@@ -692,7 +692,7 @@ view model =
renderContent : Model -> Html Msg
renderContent model =
if model.loading && List.isEmpty model.listens then
- ul [] [ li [ class "loading" ] [ text "Loading recent tracks..." ] ]
+ ul [] [ li [ class "loading" ] [ text "⏳" ] ]
else
case model.error of
@@ -802,7 +802,7 @@ renderStatsView : Set String -> Set String -> Maybe Stats -> Html Msg
renderStatsView expandedSections loadedSections mStats =
case mStats of
Nothing ->
- div [ class "loading" ] [ text "Loading stats..." ]
+ div [ class "loading" ] [ text "⏳" ]
Just stats ->
div []
@@ -873,7 +873,7 @@ renderStatSection expandedSections loadedSections mField title entries =
div [ class "stats-section" ]
[ h2 [] [ text title ]
, if List.isEmpty entries then
- div [ class "stats-empty" ] [ text "no data yet — enrichment in progress" ]
+ div [ class "stats-empty" ] [ text "no data yet" ]
else
ul [] (List.map (renderStatEntry maxCount mField) visible)
diff --git a/src/Db.purs b/src/Db.purs
@@ -20,7 +20,7 @@ import Unsafe.Coerce (unsafeCoerce)
import Types (Listen(..), TrackMetadata(..), MbidMapping(..), Stats(..), StatsEntry(..))
import Foreign.Object as Object
import Data.Nullable (Nullable, toMaybe, toNullable)
-import Data.Array (mapMaybe, uncons, (!!), last, length, replicate)
+import Data.Array (mapMaybe, (!!), last, length, null, replicate)
import Data.Foldable (for_)
import Data.String.Common (joinWith)
import Data.Tuple (Tuple(..))
@@ -46,7 +46,8 @@ connect :: String -> Aff Connection
connect path = makeAff \cb -> do
connectImpl path \err conn ->
case toMaybe err of
- Just e -> cb (Left e)
+ Just e ->
+ cb (Left e)
Nothing -> case toMaybe conn of
Just c -> cb (Right c)
Nothing -> cb (Left $ error "Failed to create connection")
@@ -99,7 +100,8 @@ backupDb conn dbFile s3cfg intervalMs slug = forever do
Left err -> do
Log.error $ "Backup failed: " <> message err
liftEffect $ Metrics.incDbBackupRun slug "error"
- Right _ -> pure unit
+ Right _ ->
+ pure unit
-- Acquires the write lock, runs the action inside a transaction, then releases.
-- The lock is always released: on success, on action failure (rollback), and on
@@ -288,7 +290,7 @@ rowToEntry json = do
pure $ StatsEntry { name, count }
getArtistReleasesByMbids :: Connection -> Array String -> Aff (Object.Object { artist :: String, release :: String })
-getArtistReleasesByMbids _ mbids | length mbids == 0 = pure Object.empty
+getArtistReleasesByMbids _ mbids | null mbids = pure Object.empty
getArtistReleasesByMbids conn mbids = do
let placeholders = joinWith "," (replicate (length mbids) "?")
rows <- queryAll conn
@@ -307,19 +309,6 @@ getArtistReleasesByMbids conn mbids = do
release <- Object.lookup "release_name" obj >>= toString
pure $ Tuple mbid { artist, release }
-getArtistReleaseByMbid :: Connection -> String -> Aff (Maybe { artist :: String, release :: String })
-getArtistReleaseByMbid conn mbid = do
- rows <- queryAll conn
- "SELECT DISTINCT artist_name, release_name FROM scrobbles WHERE release_mbid = ? AND artist_name != '' AND release_name != '' LIMIT 1"
- [ unsafeCoerce mbid ]
- pure $ case uncons rows of
- Just { head: row, tail: _ } -> do
- obj <- toObject row
- artist <- Object.lookup "artist_name" obj >>= toString
- release <- Object.lookup "release_name" obj >>= toString
- Just { artist, release }
- Nothing -> Nothing
-
rowToListen :: Json -> Maybe Listen
rowToListen json = do
obj <- toObject json
diff --git a/src/Main.purs b/src/Main.purs
@@ -36,7 +36,7 @@ import JSURI (encodeURIComponent)
import Foreign.Object as Object
import Data.Argonaut (decodeJson, encodeJson, parseJson)
import Data.Argonaut.Core (Json, toObject, toArray, toString, stringify)
-import Data.Array ((!!), length, uncons, mapMaybe, find)
+import Data.Array ((!!), length, null, mapMaybe, find)
import Data.Tuple (Tuple(..))
import Data.Foldable (for_, foldM)
import Data.Traversable (traverse)
@@ -79,7 +79,8 @@ fetchListenBrainzUrl url = withRetry "ListenBrainz fetch" $ makeAff \callback ->
launchAff_ do
result <- try $ readableToStringUtf8 (IM.toReadable res)
liftEffect $ case result of
- Left err -> callback (Left err)
+ Left err ->
+ callback (Left err)
Right body ->
if IM.statusCode res == 200 then
callback (Right body)
@@ -180,8 +181,8 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
when (added > 0 || initialSyncEnabled)
$ Log.info
$ "ListenBrainz batch: added " <> show added <> ", " <> show hitCount <> " already present."
- let allExist = hitCount == length listens && length listens > 0
- if allExist || length listens == 0 || not initialSyncEnabled then do
+ let allExist = hitCount == length listens && not (null listens)
+ if allExist || null listens || not initialSyncEnabled then do
when (added > 0) $ Log.info $ "ListenBrainz sync complete. Added " <> show added <> " new scrobbles."
recordSuccess added
else do
@@ -193,7 +194,8 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
liftEffect $ Metrics.incSyncRuns slug "listenbrainz" "error"
paginateUntilDone batchNum minTs acc = case minTs of
- Nothing -> pure acc
+ Nothing ->
+ pure acc
Just ts -> do
Log.info $ "Fetching ListenBrainz batch " <> show batchNum <> " (before " <> show ts <> ")..."
result <- try $ fetchListenBrainzUrl (listenBrainzUrl username <> "?count=100&max_ts=" <> show ts)
@@ -206,8 +208,8 @@ lbSyncOnce conn username slug writeLock initialSyncEnabled = void performFullSyn
Right (ListenBrainzResponse { payload: Payload { listens } }) -> do
Tuple added (Tuple newMinTs hitCount) <- withTransaction conn writeLock (processListens listens)
Log.info $ "ListenBrainz batch " <> show batchNum <> ": added " <> show added <> ", " <> show hitCount <> " already present."
- let allExist = hitCount == length listens && length listens > 0
- if allExist || length listens == 0 then do
+ let allExist = hitCount == length listens && not (null listens)
+ if allExist || null listens then do
pure (acc + added)
else do
paginateUntilDone (batchNum + 1) newMinTs (acc + added)
@@ -253,7 +255,7 @@ lfSyncOnce conn apiKey lfmUser slug writeLock initialSyncEnabled = do
$ "Last.fm page 1/" <> show totalPages <> ": added " <> show added <> ", " <> show hitCount <> " already present."
let
validTracks = mapMaybe lastfmTrackToListen tracks
- allExist = hitCount == length validTracks && length validTracks > 0
+ allExist = hitCount == length validTracks && not (null validTracks)
if allExist || totalPages <= 1 || not initialSyncEnabled then do
when (added > 0) $ Log.info $ "Last.fm sync complete. Added " <> show added <> " new scrobbles."
recordSuccess added
@@ -271,26 +273,23 @@ lfSyncOnce conn apiKey lfmUser slug writeLock initialSyncEnabled = do
Log.info $ "Last.fm page " <> show page <> "/" <> show totalPages <> ": added " <> show added <> ", " <> show hitCount <> " already present."
let
validTracks = mapMaybe lastfmTrackToListen tracks
- allExist = hitCount == length validTracks && length validTracks > 0
- if allExist || length tracks == 0 then pure (acc + added)
+ allExist = hitCount == length validTracks && not (null validTracks)
+ if allExist || null tracks then pure (acc + added)
else paginateLastfmUntilDone (page + 1) totalPages mTo (acc + added)
performLastfmBackfill = do
mOldest <- getOldestTs conn
- case mOldest of
- Nothing -> pure unit
- Just oldestTs -> do
- when initialSyncEnabled $ Log.info $ "Checking for Last.fm history before " <> show oldestTs <> "..."
- { tracks, totalPages } <- fetchLastfmPage apiKey lfmUser 1 (Just (oldestTs - 1))
- if length tracks == 0 then do
- when initialSyncEnabled $ Log.info "No older Last.fm history found."
- pure unit
- else do
- Log.info $ "Backfilling " <> show totalPages <> " pages of Last.fm history before " <> show oldestTs
- Tuple added hitCount <- withTransaction conn writeLock (processLastfmTracks tracks)
- Log.info $ "Last.fm backfill page 1/" <> show totalPages <> ": added " <> show added <> ", " <> show hitCount <> " already present."
- total <- paginateLastfmUntilDone 2 totalPages (Just (oldestTs - 1)) added
- Log.info $ "Last.fm backfill complete. Added " <> show total <> " older scrobbles."
+ for_ mOldest \oldestTs -> do
+ when initialSyncEnabled $ Log.info $ "Checking for Last.fm history before " <> show oldestTs <> "..."
+ { tracks, totalPages } <- fetchLastfmPage apiKey lfmUser 1 (Just (oldestTs - 1))
+ if null tracks then
+ when initialSyncEnabled $ Log.info "No older Last.fm history found."
+ else do
+ Log.info $ "Backfilling " <> show totalPages <> " pages of Last.fm history before " <> show oldestTs
+ Tuple added hitCount <- withTransaction conn writeLock (processLastfmTracks tracks)
+ Log.info $ "Last.fm backfill page 1/" <> show totalPages <> ": added " <> show added <> ", " <> show hitCount <> " already present."
+ total <- paginateLastfmUntilDone 2 totalPages (Just (oldestTs - 1)) added
+ Log.info $ "Last.fm backfill complete. Added " <> show total <> " older scrobbles."
recordSuccess n = do
liftEffect $ Metrics.incSyncRuns slug "lastfm" "success"
@@ -327,28 +326,38 @@ handleRequest metricsEnabled contexts req res = do
let method = IM.method req
let rawUrl = IM.url req
case URL.fromRelative rawUrl "http://localhost" of
- Nothing -> serveNotFound res
+ Nothing ->
+ serveNotFound res
Just url -> do
let path = URL.pathname url
Metrics.wrapRequest method (normalizePath path) Log.info req res do
case path of
- "/client.js" -> serveClientJs res
- "/favicon.png" -> serveAsset "image/png" "assets/favicon.png" res
- "/" -> serveIndex "" res
+ "/client.js" ->
+ serveClientJs res
+ "/favicon.png" ->
+ serveAsset "image/png" "assets/favicon.png" res
+ "/" ->
+ serveIndex "" res
"/metrics" ->
if metricsEnabled then serveMetrics res
else do
Log.warn "Path not found: /metrics"
serveNotFound res
- "/proxy" -> withUser url \ctx -> serveProxy ctx.conn url res
- "/stats" -> withUser url \ctx -> serveStats ctx.conn url res
- "/cover" -> withUser url \ctx -> serveCover ctx.config ctx.slug url res
- "/healthz" -> withUser url \ctx -> serveHealthz ctx.conn res
- _ -> case stripPrefix (Pattern "/~") path of
- Just slug -> serveIndex slug res
- Nothing -> do
- Log.warn $ "Path not found: " <> path
- serveNotFound res
+ "/proxy" ->
+ withUser url \ctx -> serveProxy ctx.conn url res
+ "/stats" ->
+ withUser url \ctx -> serveStats ctx.conn url res
+ "/cover" ->
+ withUser url \ctx -> serveCover ctx.config ctx.slug url res
+ "/healthz" ->
+ withUser url \ctx -> serveHealthz ctx.conn res
+ _ ->
+ case stripPrefix (Pattern "/~") path of
+ Just slug ->
+ serveIndex slug res
+ Nothing -> do
+ Log.warn $ "Path not found: " <> path
+ serveNotFound res
where
withUser url f =
let
@@ -358,7 +367,8 @@ handleRequest metricsEnabled contexts req res = do
Nothing -> do
Log.warn $ "Unknown user: " <> show slug
serveNotFound res
- Just ctx -> f ctx
+ Just ctx ->
+ f ctx
serveIndex :: String -> Response -> Effect Unit
serveIndex slug res = do
@@ -414,7 +424,8 @@ sanitizeKey = replace re1 "_" >>> replace re2 "_"
-- Fetch the Last.fm image URL for an artist/release (no caching, no serving).
fetchLastfmCoverUrl :: UserConfig -> String -> String -> Aff (Maybe String)
fetchLastfmCoverUrl cfg artist release = case cfg.lastfmApiKey of
- Nothing -> pure Nothing
+ Nothing ->
+ pure Nothing
Just k -> do
let
searchUrl = "https://ws.audioscrobbler.com/2.0/?method=album.getinfo&artist="
@@ -435,12 +446,14 @@ fetchLastfmCoverUrl cfg artist release = case cfg.lastfmApiKey of
imgObj <- images !! 2 >>= toObject
u <- Object.lookup "#text" imgObj >>= toString
if u == "" then Nothing else Just u
- _ -> pure Nothing
+ _ ->
+ pure Nothing
-- Fetch the Discogs image URL for an artist/release (no caching, no serving).
fetchDiscogsCoverUrl :: UserConfig -> String -> String -> Aff (Maybe String)
fetchDiscogsCoverUrl cfg artist release = case cfg.discogsToken of
- Nothing -> pure Nothing
+ Nothing ->
+ pure Nothing
Just t -> do
let
queryStr = artist <> " " <> release
@@ -457,7 +470,8 @@ fetchDiscogsCoverUrl cfg artist release = case cfg.discogsToken of
results <- Object.lookup "results" obj >>= toArray
firstResult <- results !! 0 >>= toObject
Object.lookup "cover_image" firstResult >>= toString
- _ -> pure Nothing
+ _ ->
+ pure Nothing
type CoverSource =
{ name :: String
@@ -514,7 +528,8 @@ serveCover cfg slug url res = launchAff_ do
else do
mUrl <- findUrl
case mUrl of
- Nothing -> pure false
+ Nothing ->
+ pure false
Just urlStr -> do
success <- tryProxyAndCache s3cfg urlStr s3Key res
liftEffect $
@@ -556,7 +571,8 @@ serveCover cfg slug url res = launchAff_ do
Right _ -> Log.info $ "Cached to S3: " <> s3Key
Left err -> Log.error $ "S3 upload failed: " <> Exception.message err
pure true
- _ -> pure false
+ _ ->
+ pure false
serveProxy :: Connection -> URL -> Response -> Effect Unit
serveProxy db url res = do
@@ -592,7 +608,8 @@ serveAsset contentType path res = do
let w = toWriteable (toOutgoingMessage res)
void $ write w buf
end w
- Left _ -> serveNotFound res
+ Left _ ->
+ serveNotFound res
serveHealthz :: Connection -> Response -> Effect Unit
serveHealthz db res = do
@@ -668,9 +685,7 @@ fetchMusicBrainzRelease mbid = do
obj <- toObject json
rg <- Object.lookup "release-group" obj >>= toObject
dateStr <- Object.lookup "first-release-date" rg >>= toString
- case uncons (String.split (Pattern "-") dateStr) of
- Just { head } -> fromString head
- Nothing -> Nothing
+ String.split (Pattern "-") dateStr !! 0 >>= fromString
Log.info $ "Enriched " <> mbid <> ": genre=" <> show genre <> " label=" <> show label <> " year=" <> show year
when (genre == Nothing && label == Nothing && year == Nothing)
@@ -808,7 +823,8 @@ enrichMetadata conn cfg slug = forever do
finalGenre <- fetchFallbackGenre slug sources
upsertReleaseMetadata conn mbid finalGenre mbdata.label mbdata.year
case finalGenre of
- Just genre -> Log.info $ "Added fallback genre for " <> mbid <> ": " <> genre
+ Just genre ->
+ Log.info $ "Added fallback genre for " <> mbid <> ": " <> genre
Nothing -> do
Log.info $ "No genre found in any source for " <> mbid
touchGenreCheckedAt conn mbid
diff --git a/test/Main.purs b/test/Main.purs
@@ -178,7 +178,8 @@ main = runSpecAndExitProcess [consoleReporter] do
}
"""
case lastfmTrackToListen j of
- Nothing -> fail "Expected Just Listen, got Nothing"
+ Nothing ->
+ fail "Expected Just Listen, got Nothing"
Just (Listen { listenedAt, trackMetadata: TrackMetadata m }) -> do
listenedAt `shouldEqual` Just 1600000000
m.trackName `shouldEqual` Just "Test Track"
@@ -196,7 +197,8 @@ main = runSpecAndExitProcess [consoleReporter] do
}
"""
case lastfmTrackToListen j of
- Nothing -> fail "Expected Just Listen, got Nothing"
+ Nothing ->
+ fail "Expected Just Listen, got Nothing"
Just (Listen { trackMetadata: TrackMetadata m }) ->
m.mbidMapping `shouldEqual` Just (MbidMapping { releaseMbid: Nothing, caaReleaseMbid: Nothing })
@@ -242,7 +244,8 @@ main = runSpecAndExitProcess [consoleReporter] do
}
"""
case lastfmTrackToListen j of
- Nothing -> fail "Expected Just Listen, got Nothing"
+ Nothing ->
+ fail "Expected Just Listen, got Nothing"
Just (Listen { trackMetadata: TrackMetadata m }) ->
m.mbidMapping `shouldEqual` Just (MbidMapping { releaseMbid: Just "mbid-xyz", caaReleaseMbid: Just "mbid-xyz" })
@@ -259,7 +262,8 @@ main = runSpecAndExitProcess [consoleReporter] do
}
"""
case lastfmTrackToListen j of
- Nothing -> fail "lastfmTrackToListen returned Nothing"
+ Nothing ->
+ fail "lastfmTrackToListen returned Nothing"
Just listen -> do
upsertScrobble conn listen
exists <- checkExists conn 1700000000