From 888b5a68e62484d19e8bb2b3985a1a9d3f1e02a9 Mon Sep 17 00:00:00 2001
From: Claudio Ortolina
Date: Tue, 14 Apr 2026 16:14:59 +0100
Subject: [PATCH] Update all callers to use matching_records
Migrate StatsLive.Index, ScrobbledTracksLive.Index, and TopAlbums to
consume matching_records list instead of scalar record IDs. Remove the
derive_legacy_record_ids bridge function. TopAlbums now uses a dropdown
for multi-record badges with play count display.
---
lib/music_library/listening_stats.ex | 23 +--
.../live/scrobbled_tracks_live/index.ex | 12 +-
.../live/stats_live/index.ex | 24 +--
.../live/stats_live/top_albums.ex | 138 ++++++++++++++----
priv/gettext/default.pot | 2 +
priv/gettext/en/LC_MESSAGES/default.po | 2 +
6 files changed, 126 insertions(+), 75 deletions(-)
diff --git a/lib/music_library/listening_stats.ex b/lib/music_library/listening_stats.ex
index 36bd0e40..6e4f7199 100644
--- a/lib/music_library/listening_stats.ex
+++ b/lib/music_library/listening_stats.ex
@@ -123,12 +123,9 @@ defmodule MusicLibrary.ListeningStats do
recent_tracks
|> Enum.map(fn %{track: track, artist_id: artist_id, matching_records: matching_records} =
rt ->
- parsed = parse_matching_records(matching_records)
-
rt
|> Map.put(:track, polyfill_track(track, timezone, artist_id))
- |> Map.put(:matching_records, parsed)
- |> derive_legacy_record_ids(parsed)
+ |> Map.put(:matching_records, parse_matching_records(matching_records))
end)
recent_albums =
@@ -200,11 +197,7 @@ defmodule MusicLibrary.ListeningStats do
from(t in ordered_query, limit: ^page_size, offset: ^offset)
|> Repo.all()
|> Enum.map(fn result ->
- parsed = parse_matching_records(result.matching_records)
-
- result
- |> Map.put(:matching_records, parsed)
- |> derive_legacy_record_ids(parsed)
+ %{result | matching_records: parse_matching_records(result.matching_records)}
end)
end
@@ -624,16 +617,4 @@ defmodule MusicLibrary.ListeningStats do
{:ok, dt, _offset} = DateTime.from_iso8601(dt_string)
dt
end
-
- # Temporary bridge: derive collected_record_id and wishlisted_record_id from
- # matching_records so existing LiveView templates keep working until they are
- # migrated to use matching_records directly.
- defp derive_legacy_record_ids(result, matching_records) do
- collected = Enum.find(matching_records, &(&1.purchased_at != nil))
- wishlisted = Enum.find(matching_records, &is_nil(&1.purchased_at))
-
- result
- |> Map.put(:collected_record_id, collected && collected.id)
- |> Map.put(:wishlisted_record_id, wishlisted && wishlisted.id)
- end
end
diff --git a/lib/music_library_web/live/scrobbled_tracks_live/index.ex b/lib/music_library_web/live/scrobbled_tracks_live/index.ex
index 0da5461d..7370dc53 100644
--- a/lib/music_library_web/live/scrobbled_tracks_live/index.ex
+++ b/lib/music_library_web/live/scrobbled_tracks_live/index.ex
@@ -107,8 +107,7 @@ defmodule MusicLibraryWeb.ScrobbledTracksLive.Index do
%{
track: track,
artist_id: artist_id,
- collected_record_id: collected_record_id,
- wishlisted_record_id: wishlisted_record_id,
+ matching_records: matching_records,
cover_hash: cover_hash
}} <- @streams.tracks
}
@@ -147,16 +146,13 @@ defmodule MusicLibraryWeb.ScrobbledTracksLive.Index do
<.record_status_badges
+ id={"status-#{track.scrobbled_at_uts}"}
musicbrainz_id={track.album.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
+ matching_records={matching_records}
/>
<.import_format_dropdown
- :if={
- track.album.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
+ :if={track.album.musicbrainz_id !== "" and matching_records == []}
id={"actions-#{track.scrobbled_at_uts}-tracks"}
musicbrainz_id={track.album.musicbrainz_id}
/>
diff --git a/lib/music_library_web/live/stats_live/index.ex b/lib/music_library_web/live/stats_live/index.ex
index 24e13736..f3e399eb 100644
--- a/lib/music_library_web/live/stats_live/index.ex
+++ b/lib/music_library_web/live/stats_live/index.ex
@@ -266,8 +266,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
%{
album: album,
artist_id: artist_id,
- collected_record_id: collected_record_id,
- wishlisted_record_id: wishlisted_record_id,
+ matching_records: matching_records,
cover_hash: cover_hash
}} <- @streams.recent_albums
}
@@ -316,16 +315,13 @@ defmodule MusicLibraryWeb.StatsLive.Index do
<.record_status_badges
+ id={"status-#{album.scrobbled_at_uts}-albums"}
musicbrainz_id={album.metadata.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
+ matching_records={matching_records}
/>
<.import_format_dropdown
- :if={
- album.metadata.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
+ :if={album.metadata.musicbrainz_id !== "" and matching_records == []}
id={"actions-#{album.scrobbled_at_uts}-albums"}
musicbrainz_id={album.metadata.musicbrainz_id}
/>
@@ -347,8 +343,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
%{
track: track,
artist_id: artist_id,
- collected_record_id: collected_record_id,
- wishlisted_record_id: wishlisted_record_id,
+ matching_records: matching_records,
cover_hash: cover_hash
}} <- @streams.recent_tracks
}
@@ -400,16 +395,13 @@ defmodule MusicLibraryWeb.StatsLive.Index do
<.record_status_badges
+ id={"status-#{track.scrobbled_at_uts}-tracks"}
musicbrainz_id={track.album.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
+ matching_records={matching_records}
/>
<.import_format_dropdown
- :if={
- track.album.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
+ :if={track.album.musicbrainz_id !== "" and matching_records == []}
id={"actions-#{track.scrobbled_at_uts}-tracks"}
musicbrainz_id={track.album.musicbrainz_id}
/>
diff --git a/lib/music_library_web/live/stats_live/top_albums.ex b/lib/music_library_web/live/stats_live/top_albums.ex
index 668e5484..bc108b40 100644
--- a/lib/music_library_web/live/stats_live/top_albums.ex
+++ b/lib/music_library_web/live/stats_live/top_albums.ex
@@ -1,8 +1,11 @@
defmodule MusicLibraryWeb.StatsLive.TopAlbums do
use MusicLibraryWeb, :html
+ import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
+
alias MusicLibrary.Assets.Transform
alias MusicLibrary.ListeningStats
+ alias MusicLibrary.Records
alias MusicLibraryWeb.StatsLive.TopByPeriod
attr :id, :string, required: true
@@ -25,7 +28,7 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do
phx-click={navigate_to_record(album)}
class={[
"flex items-center space-x-3 p-2",
- (album.collected_record_id || album.wishlisted_record_id) &&
+ navigable?(album) &&
"cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-700"
]}
>
@@ -45,46 +48,121 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do
{album.album_title}
- <.badge :if={album.album_musicbrainz_id == ""}>
- {album.play_count}
-
- <.badge :if={
- album.album_musicbrainz_id !== "" and !album.collected_record_id and
- !album.wishlisted_record_id
- }>
- {album.play_count}
-
- <.badge
- :if={album.collected_record_id}
- color="success"
- >
- {album.play_count}
-
- <.badge
- :if={album.wishlisted_record_id}
- color="warning"
- >
- {album.play_count}
-
+ <.top_album_badge album={album} />
"""
end
- defp navigate_to_record(album) do
- cond do
- album.collected_record_id ->
- JS.navigate(~p"/collection/#{album.collected_record_id}")
+ attr :album, :map, required: true
- album.wishlisted_record_id ->
- JS.navigate(~p"/wishlist/#{album.wishlisted_record_id}")
+ defp top_album_badge(assigns) do
+ assigns =
+ assigns
+ |> assign(:status, badge_status(assigns.album.matching_records))
+ |> assign(:count, length(assigns.album.matching_records))
- true ->
- nil
+ ~H"""
+ <%= case {@count, @status} do %>
+ <% {0, _} -> %>
+ <.badge>{@album.play_count}
+ <% {1, :collected} -> %>
+ <.badge color="success">{@album.play_count}
+ <% {1, :wishlisted} -> %>
+ <.badge color="warning">{@album.play_count}
+ <% {_, status} -> %>
+ <.dropdown
+ id={"top-album-#{@album.album_musicbrainz_id}"}
+ placement="bottom-end"
+ >
+ <:toggle>
+
+ {@album.play_count}
+
+
+ <.top_album_dropdown_link
+ :for={record <- @album.matching_records}
+ record={record}
+ />
+
+ <% end %>
+ """
+ end
+
+ attr :record, :map, required: true
+
+ defp top_album_dropdown_link(assigns) do
+ path =
+ if assigns.record.purchased_at,
+ do: ~p"/collection/#{assigns.record.id}",
+ else: ~p"/wishlist/#{assigns.record.id}"
+
+ assigns = assign(assigns, :path, path)
+
+ ~H"""
+ <.dropdown_link navigate={@path}>
+
+ <.badge :if={@record.purchased_at} color="success" size="sm">
+ {gettext("C")}
+
+ <.badge :if={!@record.purchased_at} color="warning" size="sm">
+ {gettext("W")}
+
+
+ {format_label(String.to_existing_atom(@record.format))} · {type_label(
+ String.to_existing_atom(@record.type)
+ )}
+
+ · {Records.Record.format_as_date(@record.purchased_at)}
+
+
+
+
+ """
+ end
+
+ defp navigate_to_record(%{matching_records: [record]}) do
+ if record.purchased_at do
+ JS.navigate(~p"/collection/#{record.id}")
+ else
+ JS.navigate(~p"/wishlist/#{record.id}")
end
end
+ defp navigate_to_record(_album), do: nil
+
+ defp navigable?(%{matching_records: [_]}), do: true
+ defp navigable?(_), do: false
+
+ defp badge_status([]), do: nil
+
+ defp badge_status(records) do
+ all_collected = Enum.all?(records, & &1.purchased_at)
+ all_wishlisted = Enum.all?(records, &is_nil(&1.purchased_at))
+
+ cond do
+ all_collected -> :collected
+ all_wishlisted -> :wishlisted
+ true -> :mixed
+ end
+ end
+
+ defp play_count_badge_classes(:collected),
+ do:
+ "bg-emerald-50 text-emerald-700 ring-1 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/20"
+
+ defp play_count_badge_classes(:wishlisted),
+ do:
+ "bg-yellow-50 text-yellow-800 ring-1 ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20"
+
+ defp play_count_badge_classes(:mixed),
+ do:
+ "bg-yellow-50 text-emerald-700 ring-1 ring-emerald-600/40 dark:bg-yellow-400/10 dark:text-emerald-400 dark:ring-emerald-400/40"
+
defp cover_url(album) when is_nil(album.cover_hash) do
album.cover_url
end
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 20ba0f96..d12031c8 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -2424,11 +2424,13 @@ msgid "Chat"
msgstr ""
#: lib/music_library_web/components/scrobble_components.ex
+#: lib/music_library_web/live/stats_live/top_albums.ex
#, elixir-autogen, elixir-format
msgid "C"
msgstr ""
#: lib/music_library_web/components/scrobble_components.ex
+#: lib/music_library_web/live/stats_live/top_albums.ex
#, elixir-autogen, elixir-format
msgid "W"
msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 0f80d4ba..8d59b5c7 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -2424,11 +2424,13 @@ msgid "Chat"
msgstr ""
#: lib/music_library_web/components/scrobble_components.ex
+#: lib/music_library_web/live/stats_live/top_albums.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "C"
msgstr ""
#: lib/music_library_web/components/scrobble_components.ex
+#: lib/music_library_web/live/stats_live/top_albums.ex
#, elixir-autogen, elixir-format
msgid "W"
msgstr ""