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]