corpus

Log | Files | Refs | README | LICENSE

commit 6b99cb9e58cdf50b7a03a51f49a8e42b117d97af
parent f055cbd9767fe690d2351bac8a2c794e4164aac0
Author: mtmn <miro@haravara.org>
Date:   Sun, 19 Apr 2026 19:47:03 +0200

feat: add search to listens section

Diffstat:
Msrc/Client.elm | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/Db.purs | 16+++++++++++++---
Msrc/Main.purs | 3++-
Msrc/Templates.purs | 44++++++++++++++++++++++++++++++++++++++++++++
Mtest/Main.purs | 24++++++++++++------------
Musers.dhall | 8++++++++
Musers.json | 11+++++++++++
7 files changed, 182 insertions(+), 27 deletions(-)

diff --git a/src/Client.elm b/src/Client.elm @@ -123,6 +123,8 @@ type alias Model = , userSlug : String , similarStates : Dict String SimilarState , allUsers : List String + , searchInput : String + , activeSearch : Maybe String } @@ -155,9 +157,11 @@ init flags = , userSlug = flags.userSlug , similarStates = Dict.empty , allUsers = flags.allUsers + , searchInput = "" + , activeSearch = Nothing } , Cmd.batch - [ fetchListens flags.userSlug 25 offset Nothing + [ fetchListens flags.userSlug 25 offset Nothing Nothing , Task.perform GotTime Time.now ] ) @@ -215,6 +219,9 @@ type Msg | CollapseSection String | FetchSimilar String String | FetchSimilarTracks String String (Result Http.Error (List SimilarTrack)) + | UpdateSearchInput String + | SubmitSearch + | ClearSearch @@ -227,7 +234,7 @@ update msg model = Tick time -> if not model.loading then ( { model | currentTime = Just time, loading = True } - , fetchListens model.userSlug model.limit model.offset model.activeFilter + , fetchListens model.userSlug model.limit model.offset model.activeFilter model.activeSearch ) else @@ -283,7 +290,7 @@ update msg model = in ( { model | offset = newOffset, loading = True } , Cmd.batch - [ fetchListens model.userSlug model.limit newOffset model.activeFilter + [ fetchListens model.userSlug model.limit newOffset model.activeFilter model.activeSearch , pushUrl ("?page=" ++ String.fromInt page) ] ) @@ -298,7 +305,7 @@ update msg model = in ( { model | offset = newOffset, loading = True } , Cmd.batch - [ fetchListens model.userSlug model.limit newOffset model.activeFilter + [ fetchListens model.userSlug model.limit newOffset model.activeFilter model.activeSearch , pushUrl ("?page=" ++ String.fromInt page) ] ) @@ -387,9 +394,9 @@ update msg model = filter = Just { field = field, value = value } in - ( { model | activeFilter = filter, offset = 0, activeTab = ListensTab, loading = True } + ( { model | activeFilter = filter, activeSearch = Nothing, searchInput = "", offset = 0, activeTab = ListensTab, loading = True } , Cmd.batch - [ fetchListens model.userSlug model.limit 0 filter + [ fetchListens model.userSlug model.limit 0 filter Nothing , pushUrl "?page=1" ] ) @@ -397,7 +404,39 @@ update msg model = ClearFilter -> ( { model | activeFilter = Nothing, offset = 0, loading = True } , Cmd.batch - [ fetchListens model.userSlug model.limit 0 Nothing + [ fetchListens model.userSlug model.limit 0 Nothing model.activeSearch + , pushUrl "?page=1" + ] + ) + + UpdateSearchInput str -> + ( { model | searchInput = str }, Cmd.none ) + + SubmitSearch -> + let + q = + String.trim model.searchInput + in + if String.isEmpty q then + ( { model | activeSearch = Nothing, offset = 0, loading = True } + , Cmd.batch + [ fetchListens model.userSlug model.limit 0 Nothing Nothing + , pushUrl "?page=1" + ] + ) + + else + ( { model | activeSearch = Just q, activeFilter = Nothing, offset = 0, loading = True } + , Cmd.batch + [ fetchListens model.userSlug model.limit 0 Nothing (Just q) + , pushUrl "?page=1" + ] + ) + + ClearSearch -> + ( { model | searchInput = "", activeSearch = Nothing, offset = 0, loading = True } + , Cmd.batch + [ fetchListens model.userSlug model.limit 0 Nothing Nothing , pushUrl "?page=1" ] ) @@ -486,8 +525,8 @@ subscriptions _ = -- HTTP -fetchListens : String -> Int -> Int -> Maybe ActiveFilter -> Cmd Msg -fetchListens userSlug limit offset mFilter = +fetchListens : String -> Int -> Int -> Maybe ActiveFilter -> Maybe String -> Cmd Msg +fetchListens userSlug limit offset mFilter mSearch = let filterParams = case mFilter of @@ -497,8 +536,16 @@ fetchListens userSlug limit offset mFilter = Just { field, value } -> "&filterField=" ++ field ++ "&filterValue=" ++ Url.percentEncode value + searchParam = + case mSearch of + Nothing -> + "" + + Just q -> + "&search=" ++ Url.percentEncode q + url = - "/proxy?user=" ++ userSlug ++ "&limit=" ++ String.fromInt limit ++ "&offset=" ++ String.fromInt offset ++ filterParams + "/proxy?user=" ++ userSlug ++ "&limit=" ++ String.fromInt limit ++ "&offset=" ++ String.fromInt offset ++ filterParams ++ searchParam in Http.get { url = url @@ -736,7 +783,25 @@ view model = , case model.activeTab of ListensTab -> div [] - [ case model.activeFilter of + [ div [ class "search-bar" ] + [ input + [ type_ "text" + , class "search-input" + , placeholder "search tracks, artists, albums, labels…" + , value model.searchInput + , onInput UpdateSearchInput + , onEnter SubmitSearch + ] + [] + , button [ class "search-btn", onClick SubmitSearch ] [ text "search" ] + , case model.activeSearch of + Just _ -> + button [ class "filter-clear", onClick ClearSearch ] [ text "✕ clear" ] + + Nothing -> + text "" + ] + , case model.activeFilter of Nothing -> text "" @@ -1201,6 +1266,22 @@ renderPeriodSelector current showInput customVal mError = +onEnter : msg -> Html.Attribute msg +onEnter msg = + on "keydown" + (D.field "key" D.string + |> D.andThen + (\key -> + if key == "Enter" then + D.succeed msg + + else + D.fail "not enter" + ) + ) + + + -- HELPERS diff --git a/src/Db.purs b/src/Db.purs @@ -177,13 +177,23 @@ upsertScrobble conn (Listen { listenedAt, trackMetadata: TrackMetadata track }) ] run conn "INSERT INTO scrobbles SELECT * FROM (SELECT ? as listened_at, ? as track_name, ? as artist_name, ? as release_name, ? as release_mbid, ? as caa_release_mbid) t WHERE NOT EXISTS (SELECT 1 FROM scrobbles WHERE listened_at = t.listened_at)" params -getScrobbles :: Connection -> Int -> Int -> Maybe { field :: FilterField, value :: String } -> Aff (Array Listen) -getScrobbles conn limit offset Nothing = do +getScrobbles :: Connection -> Int -> Int -> Maybe { field :: FilterField, value :: String } -> Maybe String -> Aff (Array Listen) +getScrobbles conn limit offset _ (Just q) = do + let pattern = "%" <> q <> "%" + 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" + <> " WHERE (s.track_name ILIKE ? OR s.artist_name ILIKE ? OR s.release_name ILIKE ? OR rm.label ILIKE ?)" + <> " ORDER BY s.listened_at DESC LIMIT ? OFFSET ?" + ) + [ unsafeCoerce pattern, unsafeCoerce pattern, unsafeCoerce pattern, unsafeCoerce pattern, unsafeCoerce limit, unsafeCoerce offset ] + pure $ mapMaybe rowToListen rows +getScrobbles conn limit offset Nothing Nothing = do 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 $ mapMaybe rowToListen rows -getScrobbles conn limit offset (Just { field, value }) = do +getScrobbles conn limit offset (Just { field, value }) Nothing = do rows <- queryAll conn (filterQuery field) [ unsafeCoerce value, unsafeCoerce limit, unsafeCoerce offset ] pure $ mapMaybe rowToListen rows diff --git a/src/Main.purs b/src/Main.purs @@ -597,7 +597,8 @@ serveProxy db url res = do value <- getQueryParam "filterValue" url pure { field, value } - listens <- getScrobbles db limit offset mFilter + let mSearch = getQueryParam "search" url + listens <- getScrobbles db limit offset mFilter mSearch let responseBody = stringify $ encodeJson { payload: { listens: listens } } liftEffect $ do diff --git a/src/Templates.purs b/src/Templates.purs @@ -568,6 +568,50 @@ indexHtml userSlug allUsers = margin: 0; } + .search-bar { + display: flex; + gap: 8px; + margin-bottom: 12px; + align-items: center; + } + + .search-input { + flex: 1; + background: #521e40; + border: 1px solid #50447f; + color: #ffffff; + padding: 6px 10px; + border-radius: 4px; + font-family: inherit; + font-size: 13px; + } + + .search-input::placeholder { + color: #9fbfe7; + opacity: 0.7; + } + + .search-input:focus { + outline: none; + border-color: #ffffff; + } + + .search-btn { + background: #50447f; + border: 1px solid #50447f; + color: #ffffff; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + } + + .search-btn:hover { + background: #6a5fa0; + border-color: #6a5fa0; + } + .about-description { color: #a0c0d0; font-size: 13px; diff --git a/test/Main.purs b/test/Main.purs @@ -140,12 +140,12 @@ main = runSpecAndExitProcess [consoleReporter] do exists2 <- checkExists conn 12345 exists2 `shouldEqual` true - listens <- getScrobbles conn 10 0 Nothing + listens <- getScrobbles conn 10 0 Nothing Nothing length listens `shouldEqual` 1 upsertReleaseMetadata conn "rb1" (Just "Rock") (Just "Label") (Just 2023) - listensWithGenre <- getScrobbles conn 10 0 Nothing + listensWithGenre <- getScrobbles conn 10 0 Nothing Nothing case listensWithGenre of [Listen { trackMetadata: TrackMetadata m }] -> m.genre `shouldEqual` Just "Rock" _ -> fail "Expected 1 listen" @@ -158,10 +158,10 @@ main = runSpecAndExitProcess [consoleReporter] do length s.tracks `shouldEqual` 1 -- Test Filtering (as mentioned in architecture.md) - listensFiltered <- getScrobbles conn 10 0 (Just { field: FilterGenre, value: "Rock" }) + listensFiltered <- getScrobbles conn 10 0 (Just { field: FilterGenre, value: "Rock" }) Nothing length listensFiltered `shouldEqual` 1 - listensEmpty <- getScrobbles conn 10 0 (Just { field: FilterGenre, value: "Jazz" }) + listensEmpty <- getScrobbles conn 10 0 (Just { field: FilterGenre, value: "Jazz" }) Nothing length listensEmpty `shouldEqual` 0 it "upsertScrobble is idempotent" do @@ -180,7 +180,7 @@ main = runSpecAndExitProcess [consoleReporter] do } upsertScrobble conn listen upsertScrobble conn listen - listens <- getScrobbles conn 10 0 Nothing + listens <- getScrobbles conn 10 0 Nothing Nothing length listens `shouldEqual` 1 describe "getScrobbles filter variants" do @@ -201,9 +201,9 @@ main = runSpecAndExitProcess [consoleReporter] do } upsertScrobble conn (mkListen 1 "Alpha") upsertScrobble conn (mkListen 2 "Beta") - listens <- getScrobbles conn 10 0 (Just { field: FilterArtist, value: "Alpha" }) + listens <- getScrobbles conn 10 0 (Just { field: FilterArtist, value: "Alpha" }) Nothing length listens `shouldEqual` 1 - listensNone <- getScrobbles conn 10 0 (Just { field: FilterArtist, value: "Gamma" }) + listensNone <- getScrobbles conn 10 0 (Just { field: FilterArtist, value: "Gamma" }) Nothing length listensNone `shouldEqual` 0 it "filters by label" do @@ -223,9 +223,9 @@ main = runSpecAndExitProcess [consoleReporter] do } ) upsertReleaseMetadata conn "mb-label" Nothing (Just "Warp") (Just 2000) - listens <- getScrobbles conn 10 0 (Just { field: FilterLabel, value: "Warp" }) + listens <- getScrobbles conn 10 0 (Just { field: FilterLabel, value: "Warp" }) Nothing length listens `shouldEqual` 1 - listensNone <- getScrobbles conn 10 0 (Just { field: FilterLabel, value: "Columbia" }) + listensNone <- getScrobbles conn 10 0 (Just { field: FilterLabel, value: "Columbia" }) Nothing length listensNone `shouldEqual` 0 it "filters by year" do @@ -245,9 +245,9 @@ main = runSpecAndExitProcess [consoleReporter] do } ) upsertReleaseMetadata conn "mb-year" Nothing Nothing (Just 1994) - listens <- getScrobbles conn 10 0 (Just { field: FilterYear, value: "1994" }) + listens <- getScrobbles conn 10 0 (Just { field: FilterYear, value: "1994" }) Nothing length listens `shouldEqual` 1 - listensNone <- getScrobbles conn 10 0 (Just { field: FilterYear, value: "1999" }) + listensNone <- getScrobbles conn 10 0 (Just { field: FilterYear, value: "1999" }) Nothing length listensNone `shouldEqual` 0 describe "getOldestTs" do @@ -497,7 +497,7 @@ main = runSpecAndExitProcess [consoleReporter] do upsertScrobble conn listen exists <- checkExists conn 1700000000 exists `shouldEqual` true - listens <- getScrobbles conn 10 0 Nothing + listens <- getScrobbles conn 10 0 Nothing Nothing length listens `shouldEqual` 1 describe "Corpus S3" do diff --git a/users.dhall b/users.dhall @@ -27,5 +27,13 @@ in { users = with databaseFile = "corpus-mtmn.db" with backupEnabled = True } + , { slug = "mtmnn" + , config = + defaults + with lastfmUser = Some "mtmnn" + with databaseFile = "corpus-lastfm-mtmnn.db" + with backupEnabled = True + with initialSync = True + } ] } diff --git a/users.json b/users.json @@ -10,6 +10,17 @@ "listenbrainzUser": "mtmn" }, "slug": "" + }, + { + "config": { + "backupEnabled": true, + "backupIntervalHours": 1, + "coverCacheEnabled": true, + "databaseFile": "corpus-lastfm-mtmnn.db", + "initialSync": true, + "lastfmUser": "mtmnn" + }, + "slug": "mtmnn" } ] }