corpus

Log | Files | Refs | README | LICENSE

commit ac540524df7d2b50eac6ba64abd1943ebff6ed3c
parent e0b2708337cc3ef8f418dc3f2098ee13fd133ba1
Author: mtmn <miro@haravara.org>
Date:   Sat, 18 Apr 2026 22:39:05 +0200

chore: linter fixes

Diffstat:
Msrc/Client.elm | 6+++---
Msrc/Db.purs | 23++++++-----------------
Msrc/Main.purs | 114+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mtest/Main.purs | 12++++++++----
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