268aff9918
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"
232 lines
7.0 KiB
Elixir
232 lines
7.0 KiB
Elixir
defmodule MusicLibraryWeb.StatsLive.Index do
|
|
use MusicLibraryWeb, :live_view
|
|
|
|
import MusicLibraryWeb.ChartComponents
|
|
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
|
|
|
|
alias MusicLibrary.{Collection, Records, ScrobbleActivity, Wishlist}
|
|
|
|
attr :record, MusicLibrary.Records.Record, required: true
|
|
attr :title, :string, required: true
|
|
attr :class, :string
|
|
|
|
defp album_preview(assigns) do
|
|
~H"""
|
|
<div
|
|
class={[
|
|
"relative overflow-hidden rounded-md bg-white dark:bg-zinc-800 px-4 pb-5 pt-5 shadow-sm sm:px-6 sm:pt-6 cursor-pointer",
|
|
@class
|
|
]}
|
|
phx-click={JS.navigate(~p"/collection/#{@record}")}
|
|
>
|
|
<dt>
|
|
<img
|
|
class="absolute w-20 rounded-md shadow-sm"
|
|
src={~p"/covers/#{@record.id}?vsn=#{@record.cover_hash}"}
|
|
alt={@record.title}
|
|
/>
|
|
<p class="ml-24 truncate text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
|
{@title}
|
|
</p>
|
|
</dt>
|
|
<dd class="ml-24 flex items-baseline pb-4 sm:pb-6">
|
|
<p class="font-semibold">
|
|
<span class="text-sm md:text-base lg:text-2xl block text-zinc-900 dark:text-zinc-300">
|
|
{@record.title}
|
|
</span>
|
|
<.link
|
|
:for={artist <- @record.artists}
|
|
class="text-sm md:text-base text-zinc-600 dark:text-zinc-200 hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
|
|
navigate={~p"/artists/#{artist.musicbrainz_id}"}
|
|
>
|
|
{artist.name}
|
|
</.link>
|
|
</p>
|
|
</dd>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :title, :string, required: true
|
|
attr :count, :integer, required: true
|
|
attr :path, :string, required: true
|
|
|
|
defp counter(assigns) do
|
|
~H"""
|
|
<div class="overflow-hidden rounded-md bg-white dark:bg-zinc-800 px-4 pb-3 pt-5 shadow-sm sm:px-6 sm:pt-6">
|
|
<dt class="sm:mt-3">
|
|
<p class="truncate text-sm font-medium text-center text-zinc-500 dark:text-zinc-400">
|
|
{@title}
|
|
</p>
|
|
</dt>
|
|
<dd class="mt-1">
|
|
<.link
|
|
navigate={@path}
|
|
class="block text-2xl sm:text-3xl font-semibold text-center text-zinc-900 hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
|
|
>
|
|
{@count}
|
|
</.link>
|
|
</dd>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
def mount(_params, _session, socket) do
|
|
latest_record = Collection.get_latest_record!()
|
|
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()
|
|
end
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:timezone, resolve_timezone!())
|
|
|> stream_configure(:recent_tracks,
|
|
dom_id: fn track -> "track-#{track.scrobbled_at_uts}" end
|
|
)
|
|
|> stream_configure(:recent_albums,
|
|
dom_id: fn album -> "album-#{album.scrobbled_at_uts}" end
|
|
)
|
|
|> assign_counts()
|
|
|> assign_scrobble_activity(recent_tracks)
|
|
|> assign(
|
|
scrobble_activity_mode: :albums,
|
|
latest_record: latest_record,
|
|
page_title: gettext("Stats"),
|
|
current_section: :stats,
|
|
records_by_artist: records_by_artists,
|
|
records_by_genre: records_by_genre,
|
|
top_albums: top_albums
|
|
)}
|
|
end
|
|
|
|
def handle_event("refresh_lastfm_feed", _, socket) do
|
|
LastFm.refresh_scrobbled_tracks()
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("import", %{"id" => musicbrainz_id, "format" => format}, socket) do
|
|
case Records.import_from_musicbrainz_release(musicbrainz_id,
|
|
format: format,
|
|
purchased_at: nil
|
|
) do
|
|
{:ok, record} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, gettext("Record wishlisted successfully"))
|
|
|> push_navigate(to: ~p"/wishlist/#{record.id}")}
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(
|
|
:error,
|
|
gettext("Error wishlisting record") <> "," <> inspect(changeset.errors)
|
|
)}
|
|
|
|
{:error, reason} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:error, gettext("Error wishlisting record") <> "," <> inspect(reason))}
|
|
end
|
|
end
|
|
|
|
def handle_event("set_scrobble_activity_mode", %{"mode" => mode}, socket)
|
|
when mode in ["tracks", "albums"] do
|
|
recent_tracks = LastFm.get_scrobbled_tracks()
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign_scrobble_activity(recent_tracks)
|
|
|> assign(scrobble_activity_mode: String.to_existing_atom(mode))}
|
|
end
|
|
|
|
def handle_info(%{track_count: 0}, socket) do
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_info(%{track_count: _count}, socket) do
|
|
recent_tracks = LastFm.get_scrobbled_tracks()
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign_scrobble_activity(recent_tracks)}
|
|
end
|
|
|
|
defp assign_counts(socket) do
|
|
collection_count_by_format = Collection.count_records_by_format()
|
|
|
|
collection_count_by_type = Collection.count_records_by_type()
|
|
|
|
collection_count =
|
|
Enum.sum_by(collection_count_by_format, fn {_, count} -> count end)
|
|
|
|
wishlist_count = Wishlist.count()
|
|
|
|
assign(socket,
|
|
collection_count_by_format: collection_count_by_format,
|
|
collection_count_by_type: collection_count_by_type,
|
|
collection_count: collection_count,
|
|
wishlist_count: wishlist_count
|
|
)
|
|
end
|
|
|
|
defp assign_scrobble_activity(socket, recent_tracks) do
|
|
%{
|
|
localized_recent_tracks: localized_recent_tracks,
|
|
localized_recent_albums: recent_albums,
|
|
collected_releases: collected_releases,
|
|
wishlisted_releases: wishlisted_releases,
|
|
artist_ids: artist_ids
|
|
} = ScrobbleActivity.from_recent_tracks(recent_tracks, socket.assigns.timezone)
|
|
|
|
socket
|
|
|> stream(:recent_tracks, localized_recent_tracks, reset: true)
|
|
|> stream(:recent_albums, recent_albums, reset: true)
|
|
|> assign(
|
|
collected_releases: collected_releases,
|
|
wishlisted_releases: wishlisted_releases,
|
|
artist_ids: artist_ids
|
|
)
|
|
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
|
|
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.
|
|
defp stats_class(collection) do
|
|
case Enum.count(collection) do
|
|
1 -> "grid-cols-1"
|
|
2 -> "grid-cols-2"
|
|
3 -> "grid-cols-3"
|
|
4 -> "grid-cols-4"
|
|
5 -> "grid-cols-5"
|
|
6 -> "grid-cols-6"
|
|
7 -> "grid-cols-7"
|
|
8 -> "grid-cols-8"
|
|
9 -> "grid-cols-9"
|
|
_other -> ""
|
|
end
|
|
end
|
|
|
|
defp format_scrobbled_at_uts(uts) do
|
|
uts
|
|
|> DateTime.from_unix!()
|
|
|> DateTime.to_iso8601()
|
|
end
|
|
|
|
defp resolve_timezone! do
|
|
Application.get_env(:music_library, MusicLibraryWeb)
|
|
|> Keyword.fetch!(:timezone)
|
|
end
|
|
end
|