Group records in records on this day widget

This commit is contained in:
Claudio Ortolina
2026-03-12 09:09:25 +00:00
parent a6348fe647
commit 1a62360b21
6 changed files with 229 additions and 40 deletions
+25
View File
@@ -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 =
@@ -163,27 +163,52 @@ defmodule MusicLibraryWeb.StatsComponents do
>
{gettext("No records released on this day.")}
</li>
<li
:for={record <- @records}
phx-click={JS.navigate(@record_show_path.(record))}
class="flex justify-between gap-x-6 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 px-2 md:px-4 cursor-pointer"
id={record.id}
>
<%= 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}} -> %>
<li id={"group-#{rep.musicbrainz_id}"} class="py-2 px-2 md:px-4">
<details class="group/details">
<summary class="flex justify-between gap-x-6 cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md -mx-2 px-2 py-1">
<div class="flex min-w-0 gap-x-4 items-center">
<div class="relative w-12 flex-none">
<.record_cover record={record} width={96} />
<.release_groups_badge record={record} />
<.record_cover record={rep} width={96} />
<.release_groups_badge record={rep} />
</div>
<div class="min-w-0 flex-auto">
<h1 class="text-sm leading-6 text-zinc-700">
<.artist_links joinphrase_class="text-xs" artists={record.artists} />
<.artist_links joinphrase_class="text-xs" artists={rep.artists} />
</h1>
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{record.title}
{rep.title}
</h2>
<p class="mt-1 text-xs leading-5 text-zinc-500 dark:text-zinc-400">
<.released_how_long_ago record={record} current_date={@current_date} />
· {format_label(record.format)} · {type_label(record.type)}
<.released_how_long_ago record={rep} current_date={@current_date} />
· {ngettext("1 release", "%{count} releases", length(records))}
</p>
</div>
</div>
<div class="flex items-center">
<.icon
name="hero-chevron-right"
class="h-4 w-4 text-zinc-400 transition-transform group-open/details:rotate-90"
/>
</div>
</summary>
<ul class="ml-16 mt-1 border-l border-zinc-200 dark:border-zinc-700 pl-4">
<li
:for={record <- records}
phx-click={JS.navigate(@record_show_path.(record))}
class="flex justify-between gap-x-6 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-700 px-2 rounded-md cursor-pointer"
id={record.id}
>
<p class="text-xs leading-5 text-zinc-500 dark:text-zinc-400">
{format_label(record.format)} · {type_label(record.type)}
<span :if={record.purchased_at}>
·
<span class="sr-only">
@@ -198,10 +223,59 @@ defmodule MusicLibraryWeb.StatsComponents do
{Records.Record.format_as_date(record.purchased_at)}
</span>
</p>
</li>
</ul>
</details>
</li>
<% end %>
<% end %>
</ul>
"""
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"""
<li
phx-click={JS.navigate(@record_show_path.(@record))}
class="flex justify-between gap-x-6 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 px-2 md:px-4 cursor-pointer"
id={@record.id}
>
<div class="flex min-w-0 gap-x-4 items-center">
<div class="relative w-12 flex-none">
<.record_cover record={@record} width={96} />
<.release_groups_badge record={@record} />
</div>
<div class="min-w-0 flex-auto">
<h1 class="text-sm leading-6 text-zinc-700">
<.artist_links joinphrase_class="text-xs" artists={@record.artists} />
</h1>
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
{@record.title}
</h2>
<p class="mt-1 text-xs leading-5 text-zinc-500 dark:text-zinc-400">
<.released_how_long_ago record={@record} current_date={@current_date} />
· {format_label(@record.format)} · {type_label(@record.type)}
<span :if={@record.purchased_at}>
·
<span class="sr-only">
{gettext("Purchased on")}
</span>
<.icon
name="hero-banknotes"
class="h-4 w-4"
aria-hidden="true"
data-slot="icon"
/>
{Records.Record.format_as_date(@record.purchased_at)}
</span>
</p>
</div>
</div>
</li>
</ul>
"""
end
@@ -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
+7
View File
@@ -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] ""
+7
View File
@@ -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] ""
+69
View File
@@ -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]