Show top artists in stats page

This commit is contained in:
Claudio Ortolina
2025-06-16 16:47:35 +03:00
parent 53e7818130
commit 9b20db762d
5 changed files with 139 additions and 0 deletions
+50
View File
@@ -240,6 +240,40 @@ defmodule MusicLibrary.ScrobbleActivity do
Repo.all(query)
end
@doc """
Gets the top artists by scrobble count for the given number of days.
Returns a list of maps with artist information and play counts.
"""
def get_top_artists_by_days(days, opts) do
limit = Keyword.get(opts, :limit, 10)
current_time = Keyword.get_lazy(opts, :current_time, &DateTime.utc_now/0)
timezone = Keyword.get(opts, :timezone, &resolve_timezone!/0)
cutoff_timestamp =
current_time
|> DateTime.add(-days, :day)
|> NaiveDateTime.beginning_of_day()
|> DateTime.from_naive!(timezone)
|> DateTime.to_unix()
query =
from t in Track,
where: t.scrobbled_at_uts >= ^cutoff_timestamp,
group_by: [
fragment("json_extract(artist, '$.name')"),
fragment("json_extract(artist, '$.musicbrainz_id')")
],
select: %{
artist_name: fragment("json_extract(artist, '$.name')"),
artist_musicbrainz_id: fragment("json_extract(artist, '$.musicbrainz_id')"),
play_count: count(t.scrobbled_at_uts)
},
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, along with collected and
@@ -268,6 +302,22 @@ defmodule MusicLibrary.ScrobbleActivity do
}
end
@doc """
Gets top artists for multiple time periods (30, 90, 365 days).
Returns a map with the results for each period.
"""
def get_top_artists_by_periods(opts) do
last_30_days = get_top_artists_by_days(30, opts)
last_90_days = get_top_artists_by_days(90, opts)
last_365_days = get_top_artists_by_days(365, opts)
%{
last_30_days: last_30_days,
last_90_days: last_90_days,
last_365_days: last_365_days
}
end
defp resolve_timezone! do
Application.get_env(:music_library, MusicLibraryWeb)
|> Keyword.fetch!(:timezone)
@@ -150,6 +150,54 @@ defmodule MusicLibraryWeb.StatsLive.Index do
"""
end
attr :artists, :list, required: true
attr :title, :string, required: true
def top_artists_by_period(assigns) do
~H"""
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
{@title}
</h2>
<div class="space-y-2">
<div :for={artist <- @artists} class="flex items-center space-x-3 p-2">
<img
:if={artist.artist_musicbrainz_id != ""}
class="w-12 h-12 rounded-md object-cover"
src={~p"/artists/#{artist.artist_musicbrainz_id}/image"}
alt={artist.artist_name}
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
/>
<div
:if={artist.artist_musicbrainz_id == ""}
class="w-12 h-12 rounded-md bg-zinc-200 dark:bg-zinc-700 flex items-center justify-center"
>
<.icon name="hero-user" class="w-6 h-6 text-zinc-400" />
</div>
<div class="flex-1 min-w-0">
<.link
:if={artist.artist_musicbrainz_id != ""}
class="text-sm font-medium text-zinc-900 dark:text-zinc-300 hover:text-zinc-700 dark:hover:text-zinc-400 truncate"
navigate={~p"/artists/#{artist.artist_musicbrainz_id}"}
>
{artist.artist_name}
</.link>
<p
:if={artist.artist_musicbrainz_id == ""}
class="text-sm font-medium text-zinc-500 dark:text-zinc-400 truncate"
>
{artist.artist_name}
</p>
</div>
<.badge>
{artist.play_count}
</.badge>
</div>
</div>
</div>
"""
end
defp refresh_lastfm_feed_button(assigns) do
~H"""
<button
@@ -185,6 +233,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
|> assign_counts()
|> assign_scrobble_activity(recent_tracks)
|> assign_top_albums()
|> assign_top_artists()
|> assign(
scrobble_activity_mode: :albums,
latest_record: latest_record,
@@ -246,6 +295,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
{:noreply,
socket
|> assign_top_albums()
|> assign_top_artists()
|> assign_scrobble_activity(recent_tracks)}
end
@@ -300,6 +350,20 @@ defmodule MusicLibraryWeb.StatsLive.Index do
assign(socket, top_albums: top_albums)
end
defp assign_top_artists(socket) do
timezone = socket.assigns.timezone
current_time = DateTime.utc_now()
top_artists =
ScrobbleActivity.get_top_artists_by_periods(
limit: 10,
current_time: current_time,
timezone: timezone
)
assign(socket, top_artists: top_artists)
end
defp 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
@@ -98,6 +98,21 @@
</div>
</div>
<div>
<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">
{gettext("Top Artists")}
</h1>
<.refresh_lastfm_feed_button />
</div>
<div class="mt-5 grid grid-cols-1 lg:grid-cols-3 gap-5">
<.top_artists_by_period artists={@top_artists.last_30_days} title={gettext("Last 30 days")} />
<.top_artists_by_period artists={@top_artists.last_90_days} title={gettext("Last 90 days")} />
<.top_artists_by_period artists={@top_artists.last_365_days} title={gettext("Last year")} />
</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">
+5
View File
@@ -908,3 +908,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/music_library_web/live/stats_live/index.html.heex
#, elixir-autogen, elixir-format
msgid "Top Artists"
msgstr ""
+5
View File
@@ -908,3 +908,8 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/music_library_web/live/stats_live/index.html.heex
#, elixir-autogen, elixir-format, fuzzy
msgid "Top Artists"
msgstr ""