Group records in records on this day widget
This commit is contained in:
@@ -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,48 +163,122 @@ 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}
|
||||
>
|
||||
<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>
|
||||
<%= 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={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={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">
|
||||
{rep.title}
|
||||
</h2>
|
||||
<p class="mt-1 text-xs leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
<.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">
|
||||
{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>
|
||||
</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>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :record, Records.Record, required: true
|
||||
attr :current_date, Date, required: true
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] ""
|
||||
|
||||
@@ -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] ""
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user