Add top albums to the stats page

Authored with Claude.

> I'd like to have a new section in the Stats page. In this section I
want to see the top albums I listened to in the past 30,
  90, 365 days. Use the data in `scrobbled_tracks` to determine which
albums need to be displayed.

> This all looks great as a starter. I'd like to make some
modifications.

  1. Invert the position of the artist name and the album title (so that
it's consistent with the Scrobble Activity)
  2. Remove the border and shadow from around each album
  3. Rename the "Last 365 days" section to "Last Year"
This commit is contained in:
Claudio Ortolina
2025-06-14 09:38:44 +03:00
parent 131b48e963
commit 268aff9918
5 changed files with 175 additions and 3 deletions
+46 -2
View File
@@ -1,7 +1,9 @@
defmodule MusicLibrary.ScrobbleActivity do
alias LastFm.Scrobble
import Ecto.Query
alias LastFm.{Scrobble, Track}
alias MusicBrainz.Release
alias MusicLibrary.{Artists, Collection, Secrets, Wishlist}
alias MusicLibrary.{Artists, Collection, Repo, Secrets, Wishlist}
def can_scrobble? do
Secrets.get("last_fm_session_key") !== nil
@@ -200,4 +202,46 @@ defmodule MusicLibrary.ScrobbleActivity do
|> Enum.reject(fn musicbrainz_id -> musicbrainz_id == "" end)
|> MapSet.new()
end
@doc """
Gets the top albums by scrobble count for the given number of days.
Returns a list of maps with album information and play counts.
"""
def get_top_albums_by_days(days, limit \\ 10) do
cutoff_timestamp =
DateTime.utc_now()
|> DateTime.add(-days, :day)
|> DateTime.to_unix()
query =
from t in Track,
where: t.scrobbled_at_uts >= ^cutoff_timestamp,
group_by: [
fragment("json_extract(album, '$.title')"),
fragment("json_extract(artist, '$.name')")
],
select: %{
album_title: fragment("json_extract(album, '$.title')"),
artist_name: fragment("json_extract(artist, '$.name')"),
play_count: count(t.scrobbled_at_uts),
cover_url: fragment("max(?)", t.cover_url),
album_mbid: fragment("json_extract(album, '$.musicbrainz_id')")
},
order_by: [desc: count(t.scrobbled_at_uts)],
limit: ^limit
Repo.all(query)
end
@doc """
Gets top albums for multiple time periods (30, 90, 365 days).
Returns a map with the results for each period.
"""
def get_top_albums_by_periods(limit \\ 10) do
%{
last_30_days: get_top_albums_by_days(30, limit),
last_90_days: get_top_albums_by_days(90, limit),
last_365_days: get_top_albums_by_days(365, limit)
}
end
end
@@ -76,6 +76,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
recent_tracks = LastFm.get_scrobbled_tracks()
records_by_artists = Collection.count_records_by_artist(limit: 20)
records_by_genre = Collection.count_records_by_genre(limit: 20)
top_albums = ScrobbleActivity.get_top_albums_by_periods(10)
if connected?(socket) do
LastFm.subscribe_to_feed()
@@ -98,7 +99,8 @@ defmodule MusicLibraryWeb.StatsLive.Index do
page_title: gettext("Stats"),
current_section: :stats,
records_by_artist: records_by_artists,
records_by_genre: records_by_genre
records_by_genre: records_by_genre,
top_albums: top_albums
)}
end
@@ -112,6 +112,92 @@
</div>
</div>
<div>
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
{gettext("Top Albums")}
</h1>
<div class="mt-5 grid grid-cols-1 lg:grid-cols-3 gap-5">
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
{gettext("Last 30 days")}
</h2>
<div class="space-y-2">
<div :for={album <- @top_albums.last_30_days} class="flex items-center space-x-3 p-2">
<img
class="w-12 h-12 rounded-md object-cover"
src={album.cover_url}
alt={album.album_title}
/>
<div class="flex-1 min-w-0">
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
{album.artist_name}
</p>
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
{album.album_title}
</p>
</div>
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
{album.play_count}
</div>
</div>
</div>
</div>
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
{gettext("Last 90 days")}
</h2>
<div class="space-y-2">
<div :for={album <- @top_albums.last_90_days} class="flex items-center space-x-3 p-2">
<img
class="w-12 h-12 rounded-md object-cover"
src={album.cover_url}
alt={album.album_title}
/>
<div class="flex-1 min-w-0">
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
{album.artist_name}
</p>
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
{album.album_title}
</p>
</div>
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
{album.play_count}
</div>
</div>
</div>
</div>
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
{gettext("Last Year")}
</h2>
<div class="space-y-2">
<div :for={album <- @top_albums.last_365_days} class="flex items-center space-x-3 p-2">
<img
class="w-12 h-12 rounded-md object-cover"
src={album.cover_url}
alt={album.album_title}
/>
<div class="flex-1 min-w-0">
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
{album.artist_name}
</p>
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
{album.album_title}
</p>
</div>
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
{album.play_count}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flow-root">
<div class="mt-5 flex justify-between items-center">
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">