From aca5c80e4d98d0b772f94119913b7762ec158fcd Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Fri, 26 Sep 2025 15:20:16 +0300 Subject: [PATCH] Resolve all data in one query in top albums --- lib/music_library/scrobble_activity.ex | 88 +++++++++++++------ .../components/stats_components.ex | 6 -- .../live/stats_live/top_albums.ex | 54 ++++++------ 3 files changed, 88 insertions(+), 60 deletions(-) diff --git a/lib/music_library/scrobble_activity.ex b/lib/music_library/scrobble_activity.ex index 190a7384..7e509b72 100644 --- a/lib/music_library/scrobble_activity.ex +++ b/lib/music_library/scrobble_activity.ex @@ -3,7 +3,7 @@ defmodule MusicLibrary.ScrobbleActivity do alias LastFm.{Scrobble, Track} alias MusicBrainz.Release - alias MusicLibrary.{Artists, Collection, Records.ArtistRecord, Repo, Secrets, Wishlist} + alias MusicLibrary.{Artists, Records.ArtistRecord, Repo, Secrets} def can_scrobble? do Secrets.get("last_fm_session_key") !== nil @@ -232,8 +232,30 @@ defmodule MusicLibrary.ScrobbleActivity do |> DateTime.from_naive!(timezone) |> DateTime.to_unix() + collected_releases_query = + from r in fragment("records, json_each(records.release_ids)"), + where: fragment("records.purchased_at IS NOT NULL"), + select: %{ + record_id: fragment("records.id"), + cover_hash: fragment("records.cover_hash"), + release_id: r.value + } + + wishlisted_releases_query = + from r in fragment("records, json_each(records.release_ids)"), + where: fragment("records.purchased_at IS NULL"), + select: %{ + record_id: fragment("records.id"), + cover_hash: fragment("records.cover_hash"), + release_id: r.value + } + query = from t in Track, + left_join: cr in subquery(collected_releases_query), + on: cr.release_id == fragment("? ->> '$.musicbrainz_id'", t.album), + left_join: wr in subquery(wishlisted_releases_query), + on: wr.release_id == fragment("? ->> '$.musicbrainz_id'", t.album), where: t.scrobbled_at_uts >= ^cutoff_timestamp, where: fragment("json_extract(album, '$.title') != ''"), group_by: [ @@ -246,7 +268,10 @@ defmodule MusicLibrary.ScrobbleActivity do artist_musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"), play_count: count(t.scrobbled_at_uts), cover_url: fragment("max(?)", t.cover_url), - album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')") + album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')"), + collected_record_id: cr.record_id, + wishlisted_record_id: wr.record_id, + cover_hash: coalesce(cr.cover_hash, wr.cover_hash) }, order_by: [desc: count(t.scrobbled_at_uts)], limit: ^limit @@ -261,8 +286,30 @@ defmodule MusicLibrary.ScrobbleActivity do def get_top_albums(opts) do limit = Keyword.get(opts, :limit, 10) + collected_releases_query = + from r in fragment("records, json_each(records.release_ids)"), + where: fragment("records.purchased_at IS NOT NULL"), + select: %{ + record_id: fragment("records.id"), + cover_hash: fragment("records.cover_hash"), + release_id: r.value + } + + wishlisted_releases_query = + from r in fragment("records, json_each(records.release_ids)"), + where: fragment("records.purchased_at IS NULL"), + select: %{ + record_id: fragment("records.id"), + cover_hash: fragment("records.cover_hash"), + release_id: r.value + } + query = from t in Track, + left_join: cr in subquery(collected_releases_query), + on: cr.release_id == fragment("? ->> '$.musicbrainz_id'", t.album), + left_join: wr in subquery(wishlisted_releases_query), + on: wr.release_id == fragment("? ->> '$.musicbrainz_id'", t.album), where: fragment("json_extract(album, '$.title') != ''"), group_by: [ fragment("json_extract(album, '$.title')"), @@ -274,7 +321,10 @@ defmodule MusicLibrary.ScrobbleActivity do artist_musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"), play_count: count(t.scrobbled_at_uts), cover_url: fragment("max(?)", t.cover_url), - album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')") + album_musicbrainz_id: fragment("json_extract(album, '$.musicbrainz_id')"), + collected_record_id: cr.record_id, + wishlisted_record_id: wr.record_id, + cover_hash: coalesce(cr.cover_hash, wr.cover_hash) }, order_by: [desc: count(t.scrobbled_at_uts)], limit: ^limit @@ -352,31 +402,13 @@ defmodule MusicLibrary.ScrobbleActivity do releases. """ def get_top_albums_by_period(opts) do - period = Keyword.get(opts, :period, :last_7_days) - - albums = - case period do - :all_time -> get_top_albums(opts) - :last_7_days -> get_top_albums_by_days(7, opts) - :last_30_days -> get_top_albums_by_days(30, opts) - :last_90_days -> get_top_albums_by_days(90, opts) - :last_365_days -> get_top_albums_by_days(365, opts) - end - - all_album_ids = - albums - |> Enum.map(fn t -> t.album_musicbrainz_id end) - |> Enum.uniq() - |> Enum.reject(fn musicbrainz_id -> musicbrainz_id == "" end) - - collected_releases = Collection.collected_releases(all_album_ids) - wishlisted_releases = Wishlist.wishlisted_releases(all_album_ids) - - %{ - collected_releases: collected_releases, - wishlisted_releases: wishlisted_releases, - albums: albums - } + case Keyword.get(opts, :period, :last_7_days) do + :all_time -> get_top_albums(opts) + :last_7_days -> get_top_albums_by_days(7, opts) + :last_30_days -> get_top_albums_by_days(30, opts) + :last_90_days -> get_top_albums_by_days(90, opts) + :last_365_days -> get_top_albums_by_days(365, opts) + end end @doc """ diff --git a/lib/music_library_web/components/stats_components.ex b/lib/music_library_web/components/stats_components.ex index 7618c23f..ec99c9ca 100644 --- a/lib/music_library_web/components/stats_components.ex +++ b/lib/music_library_web/components/stats_components.ex @@ -246,12 +246,6 @@ defmodule MusicLibraryWeb.StatsComponents do defp silver_year?(year), do: rem(year, 5) == 0 defp normal_year?(year), do: !gold_year?(year) && !silver_year?(year) - def tracked_record?(tracked_releases, release_id) do - Enum.find_value(tracked_releases, fn tracked_release -> - if tracked_release.release_id == release_id, do: tracked_release.record_id - end) - end - # The Tailwind build step requires all needed classes to be explicitly referenced # in the source code, and not dynamically generated. This implies that one cannot # (for example) interpolate a number in a class name. 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 bd4056d9..a33d415a 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,7 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do use MusicLibraryWeb, :live_component - import MusicLibraryWeb.StatsComponents, only: [tracked_record?: 2] - + alias MusicLibrary.Assets.Transform alias MusicLibrary.ScrobbleActivity def live(assigns) do @@ -67,11 +66,7 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do <.loading /> - <.top_albums_by_period - albums={top_albums.albums} - collected_releases={top_albums.collected_releases} - wishlisted_releases={top_albums.wishlisted_releases} - /> + <.top_albums_by_period albums={top_albums} /> @@ -104,8 +99,6 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do end attr :albums, :list, required: true - attr :collected_releases, :list, required: true - attr :wishlisted_releases, :list, required: true defp top_albums_by_period(assigns) do ~H""" @@ -113,18 +106,16 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do
{album.album_title}
@@ -142,22 +133,19 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do {album.play_count} <.badge :if={ - album.album_musicbrainz_id !== "" and - !tracked_record?( - @collected_releases ++ @wishlisted_releases, - album.album_musicbrainz_id - ) + album.album_musicbrainz_id !== "" and !album.collected_record_id and + !album.wishlisted_record_id }> {album.play_count} <.badge - :if={tracked_record?(@collected_releases, album.album_musicbrainz_id)} + :if={album.collected_record_id} color="success" > {album.play_count} <.badge - :if={tracked_record?(@wishlisted_releases, album.album_musicbrainz_id)} + :if={album.wishlisted_record_id} color="warning" > {album.play_count} @@ -190,16 +178,30 @@ defmodule MusicLibraryWeb.StatsLive.TopAlbums do ) end - defp navigate_to_record(collected_releases, wishlisted_releases, musicbrainz_id) do + defp navigate_to_record(album) do cond do - record_id = tracked_record?(collected_releases, musicbrainz_id) -> - JS.navigate(~p"/collection/#{record_id}") + album.collected_record_id -> + JS.navigate(~p"/collection/#{album.collected_record_id}") - record_id = tracked_record?(wishlisted_releases, musicbrainz_id) -> - JS.navigate(~p"/wishlist/#{record_id}") + album.wishlisted_record_id -> + JS.navigate(~p"/wishlist/#{album.wishlisted_record_id}") true -> nil end end + + @last_fm_fallback_cover_url "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png" + + defp cover_url(album) do + if album.cover_url == @last_fm_fallback_cover_url do + payload = + Transform.new(hash: album.cover_hash, width: 96) + |> Transform.encode!() + + ~p"/assets/#{payload}" + else + album.cover_url + end + end end