From 201c2edc0162b8f4a1dd47dce733730a9ba8b0a0 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 10 Oct 2024 09:56:36 +0100 Subject: [PATCH] Enable tagged search --- lib/music_library/records.ex | 57 +++++++++++----- lib/music_library/records/search_parser.ex | 6 +- .../live/record_index_test.exs | 67 +++++++++++++++++++ 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/lib/music_library/records.ex b/lib/music_library/records.ex index 9957bc03..ca06d1f0 100644 --- a/lib/music_library/records.ex +++ b/lib/music_library/records.ex @@ -2,7 +2,7 @@ defmodule MusicLibrary.Records do import Ecto.Query, warn: false alias MusicLibrary.Repo - alias MusicLibrary.Records.Record + alias MusicLibrary.Records.{Record, SearchParser} @fields [:id, :type, :artists, :format, :title, :release, :genres, :musicbrainz_id, :cover_hash] @@ -24,15 +24,46 @@ defmodule MusicLibrary.Records do limit = Keyword.get(opts, :limit, 20) offset = Keyword.get(opts, :offset, 0) - q = - from r in Record, - where: like(r.title, ^"%#{query}%") or like(r.artists, ^"%#{query}%"), - order_by: [r.artists[0]["sort_name"], r.title], - limit: ^limit, - offset: ^offset, - select: ^@fields + search = + query + |> build_search() + |> limit(^limit) + |> offset(^offset) + |> select(^@fields) - Repo.all(q) + Repo.all(search) + end + + def search_records_count(query) do + search = build_search(query) + + Repo.aggregate(search, :count) + end + + defp build_search(query) do + {:ok, parsed_query} = SearchParser.parse(query) + + base_search = + from r in Record, + order_by: [r.artists[0]["sort_name"], r.title] + + Enum.reduce(parsed_query, base_search, fn + {:artist, artist}, search -> + search |> where([r], like(r.artists, ^"%#{artist}%")) + + {:album, album}, search -> + search |> where([r], like(r.title, ^"%#{album}%")) + + {:mbid, mbid}, search -> + search |> where([r], r.musicbrainz_id == ^mbid) + + {:query, raw_query}, search -> + search + |> where( + [r], + like(r.title, ^"%#{raw_query}%") or like(r.artists, ^"%#{raw_query}%") + ) + end) end def count_records do @@ -48,14 +79,6 @@ defmodule MusicLibrary.Records do Repo.all(q) end - def search_records_count(query) do - q = - from r in Record, - where: like(r.title, ^"%#{query}%") or like(r.artists, ^"%#{query}%") - - Repo.aggregate(q, :count) - end - def get_record!(id), do: Repo.get!(Record, id) def get_latest_record! do diff --git a/lib/music_library/records/search_parser.ex b/lib/music_library/records/search_parser.ex index ebe497ae..d1e05cdc 100644 --- a/lib/music_library/records/search_parser.ex +++ b/lib/music_library/records/search_parser.ex @@ -28,14 +28,18 @@ defmodule MusicLibrary.Records.SearchParser do Parse a search query. iex> MusicLibrary.Records.SearchParser.parse("") - {:ok, %{}} + {:ok, %{query: ""}} iex> MusicLibrary.Records.SearchParser.parse("marbles") {:ok, %{query: "marbles"}} iex> MusicLibrary.Records.SearchParser.parse("artist:marillion album:marbles") {:ok, %{artist: "marillion", album: "marbles"}} iex> MusicLibrary.Records.SearchParser.parse("artist:marillion album:fugazi artist:fish") {:ok, %{artist: "marillion fish", album: "fugazi"}} + iex> MusicLibrary.Records.SearchParser.parse(~s(artist:"the pineapple thief" wilderness)) + {:ok, %{artist: "the pineapple thief", query: "wilderness"}} """ + def parse(""), do: {:ok, %{query: ""}} + def parse(query) do {:ok, result, _rest, _context, _line, _byte_offset} = search_parser(query) {:ok, normalize(result)} diff --git a/test/music_library_web/live/record_index_test.exs b/test/music_library_web/live/record_index_test.exs index 0ca8d4a8..e98bd72a 100644 --- a/test/music_library_web/live/record_index_test.exs +++ b/test/music_library_web/live/record_index_test.exs @@ -96,6 +96,73 @@ defmodule MusicLibraryWeb.RecordIndexTest do end end + describe "Tagged search" do + setup [:create_records] + + test "supports raw queries", %{conn: conn, records: records} do + [record | _rest] = records + qs = [query: record.title] + {:ok, index_live, _html} = live(conn, ~p"/records?#{qs}") + + record_row = + index_live + |> with_target("#records-#{record.id}") + + record_row_html = record_row |> render() + + assert record_row_html =~ escape(record.title) + assert record_row_html =~ to_string(record.release) + assert record_row_html =~ Record.format_short_label(record.format) + assert record_row_html =~ record.release + assert record_row_html =~ ~p"/covers/#{record.id}?vsn=#{record.cover_hash}" + + for artist <- record.artists do + assert record_row_html =~ escape(artist.name) + end + end + + test "supports filters", %{conn: conn, records: records} do + {artist_with_most_records, _records_count} = + records + |> Enum.frequencies_by(fn r -> + [artist] = r.artists + artist.name + end) + |> Enum.max_by(fn {_artist, count} -> count end) + + {present, absent} = + Enum.split_with(records, fn r -> + [artist] = r.artists + artist.name == artist_with_most_records + end) + + qs = [query: ~s(artist:"#{artist_with_most_records}"), page_size: 30] + {:ok, index_live, _html} = live(conn, ~p"/records?#{qs}") + + for record <- present do + record_row = + index_live + |> with_target("#records-#{record.id}") + + record_row_html = record_row |> render() + + assert record_row_html =~ escape(record.title) + assert record_row_html =~ to_string(record.release) + assert record_row_html =~ Record.format_short_label(record.format) + assert record_row_html =~ record.release + assert record_row_html =~ ~p"/covers/#{record.id}?vsn=#{record.cover_hash}" + + for artist <- record.artists do + assert record_row_html =~ escape(artist.name) + end + end + + for record <- absent do + refute has_element?(index_live, "#records-#{record.id}") + end + end + end + describe "Updating record metadata" do test "can navigate to the record edit form", %{conn: conn} do record = record_fixture()