diff --git a/lib/music_library/collection.ex b/lib/music_library/collection.ex index facde97a..059eb4e7 100644 --- a/lib/music_library/collection.ex +++ b/lib/music_library/collection.ex @@ -61,6 +61,31 @@ defmodule MusicLibrary.Collection do Repo.all(q) end + @type grouped_record :: + {:single, SearchIndex.t()} + | {:group, %{representative: SearchIndex.t(), records: [SearchIndex.t()]}} + + @spec group_records_by_release_group([SearchIndex.t()]) :: [grouped_record()] + def group_records_by_release_group(records) do + records + |> Enum.group_by(& &1.musicbrainz_id) + |> Enum.map(fn + {_mbid, [single]} -> + {:single, single} + + {_mbid, [first | _] = group} -> + sorted = Enum.sort_by(group, & &1.purchased_at, DateTime) + {:group, %{representative: first, records: sorted}} + end) + |> Enum.sort_by( + fn + {:single, r} -> r.release_date + {:group, %{representative: r}} -> r.release_date + end, + :desc + ) + end + @spec get_latest_record() :: SearchIndex.t() | nil def get_latest_record do q = diff --git a/lib/music_library_web/components/stats_components.ex b/lib/music_library_web/components/stats_components.ex index bc150020..d83b980c 100644 --- a/lib/music_library_web/components/stats_components.ex +++ b/lib/music_library_web/components/stats_components.ex @@ -163,48 +163,122 @@ defmodule MusicLibraryWeb.StatsComponents do > {gettext("No records released on this day.")} -
  • -
    -
    - <.record_cover record={record} width={96} /> - <.release_groups_badge record={record} /> -
    -
    -

    - <.artist_links joinphrase_class="text-xs" artists={record.artists} /> -

    -

    - {record.title} -

    -

    - <.released_how_long_ago record={record} current_date={@current_date} /> - · {format_label(record.format)} · {type_label(record.type)} - - · - - {gettext("Purchased on")} - - <.icon - name="hero-banknotes" - class="h-4 w-4" - aria-hidden="true" - data-slot="icon" - /> - {Records.Record.format_as_date(record.purchased_at)} - -

    -
    -
    -
  • + <%= for entry <- @records do %> + <%= case entry do %> + <% {:single, record} -> %> + <.record_on_this_day_item + record={record} + current_date={@current_date} + record_show_path={@record_show_path} + /> + <% {:group, %{representative: rep, records: records}} -> %> +
  • +
    + +
    +
    + <.record_cover record={rep} width={96} /> + <.release_groups_badge record={rep} /> +
    +
    +

    + <.artist_links joinphrase_class="text-xs" artists={rep.artists} /> +

    +

    + {rep.title} +

    +

    + <.released_how_long_ago record={rep} current_date={@current_date} /> + · {ngettext("1 release", "%{count} releases", length(records))} +

    +
    +
    +
    + <.icon + name="hero-chevron-right" + class="h-4 w-4 text-zinc-400 transition-transform group-open/details:rotate-90" + /> +
    +
    + +
    +
  • + <% end %> + <% end %> """ end + attr :record, Records.Record, required: true + attr :current_date, Date, required: true + attr :record_show_path, :any, required: true + + defp record_on_this_day_item(assigns) do + ~H""" +
  • +
    +
    + <.record_cover record={@record} width={96} /> + <.release_groups_badge record={@record} /> +
    +
    +

    + <.artist_links joinphrase_class="text-xs" artists={@record.artists} /> +

    +

    + {@record.title} +

    +

    + <.released_how_long_ago record={@record} current_date={@current_date} /> + · {format_label(@record.format)} · {type_label(@record.type)} + + · + + {gettext("Purchased on")} + + <.icon + name="hero-banknotes" + class="h-4 w-4" + aria-hidden="true" + data-slot="icon" + /> + {Records.Record.format_as_date(@record.purchased_at)} + +

    +
    +
    +
  • + """ + end + attr :record, Records.Record, required: true attr :current_date, Date, required: true diff --git a/lib/music_library_web/live/stats_live/index.ex b/lib/music_library_web/live/stats_live/index.ex index 304ca1ae..964fbdc9 100644 --- a/lib/music_library_web/live/stats_live/index.ex +++ b/lib/music_library_web/live/stats_live/index.ex @@ -348,7 +348,11 @@ defmodule MusicLibraryWeb.StatsLive.Index do latest_record = Collection.get_latest_record() records_by_artists = Collection.count_records_by_artist(limit: 20) records_by_genre = Collection.count_records_by_genre(limit: 20) - records_on_this_day = Collection.get_records_on_this_day(current_date) + + records_on_this_day = + current_date + |> Collection.get_records_on_this_day() + |> Collection.group_records_by_release_group() if connected?(socket) do LastFm.subscribe_to_feed() @@ -413,7 +417,10 @@ defmodule MusicLibraryWeb.StatsLive.Index do def handle_event("set_current_date", %{"current_date" => current_date}, socket) do case Date.from_iso8601(current_date) do {:ok, date} -> - records_on_this_day = Collection.get_records_on_this_day(date) + records_on_this_day = + date + |> Collection.get_records_on_this_day() + |> Collection.group_records_by_release_group() {:noreply, socket diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 7fd89692..1c1c5ed9 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2284,3 +2284,10 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Notes" msgstr "" + +#: lib/music_library_web/components/stats_components.ex +#, elixir-autogen, elixir-format +msgid "1 release" +msgid_plural "%{count} releases" +msgstr[0] "" +msgstr[1] "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 8000de8c..94e233dd 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2284,3 +2284,10 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Notes" msgstr "" + +#: lib/music_library_web/components/stats_components.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "1 release" +msgid_plural "%{count} releases" +msgstr[0] "" +msgstr[1] "" diff --git a/test/music_library/collection_test.exs b/test/music_library/collection_test.exs index 98f9ddd7..870afa2c 100644 --- a/test/music_library/collection_test.exs +++ b/test/music_library/collection_test.exs @@ -87,6 +87,75 @@ defmodule MusicLibrary.CollectionTest do end end + describe "group_records_by_release_group/1" do + test "wraps single records in {:single, record} tuples" do + record = + record_with_artist("Marillion", %{ + title: "Brave", + release_date: "2020", + purchased_at: ~U[2024-12-27 16:50:57Z] + }) + + assert [{:single, ^record}] = Collection.group_records_by_release_group([record]) + end + + test "groups records sharing the same musicbrainz_id" do + shared_mbid = Ecto.UUID.generate() + + cd = + record_with_artist("Marillion", %{ + title: "Brave", + musicbrainz_id: shared_mbid, + format: :cd, + release_date: "2020", + purchased_at: ~U[2024-12-27 16:50:57Z] + }) + + vinyl = + record_with_artist("Marillion", %{ + title: "Brave", + musicbrainz_id: shared_mbid, + format: :vinyl, + release_date: "2020", + purchased_at: ~U[2024-12-28 16:50:57Z] + }) + + result = Collection.group_records_by_release_group([cd, vinyl]) + + assert [{:group, %{representative: rep, records: records}}] = result + assert rep.id == cd.id + assert length(records) == 2 + # Sorted by purchased_at ascending + assert Enum.map(records, & &1.id) == [cd.id, vinyl.id] + end + + test "sorts groups by release_date descending" do + older = + record_with_artist("Marillion", %{ + title: "Script for a Jester's Tear", + release_date: "1983", + purchased_at: ~U[2024-12-27 16:50:57Z] + }) + + newer = + record_with_artist("Marillion", %{ + title: "Brave", + release_date: "1994", + purchased_at: ~U[2024-12-28 16:50:57Z] + }) + + result = Collection.group_records_by_release_group([older, newer]) + + assert [{:single, first}, {:single, second}] = result + assert first.id == newer.id + assert second.id == older.id + end + + test "returns empty list for empty input" do + assert [] == Collection.group_records_by_release_group([]) + end + end + describe "get_latest_record!/0" do setup [:fill_collection]