defmodule MusicLibraryWeb.CollectionLive.Show do use MusicLibraryWeb, :live_view import MusicLibrary.ScrobbleActivity, only: [localize_scrobbled_at: 2] import MusicLibraryWeb.RecordComponents, only: [ format_label: 1, type_label: 1, release_summary: 1, artist_links: 1, record_colors: 1, record_cover: 1, release_list: 1, similar_records: 1 ] alias MusicLibrary.{Records, RecordSets, ScrobbleActivity} alias MusicLibrary.Records.Similarity alias MusicLibrary.RecordSets.RecordSet alias Phoenix.LiveView.JS @impl true def render(assigns) do ~H"""
<.record_cover record={@record} class="w-full rounded-lg drop-shadow-sm" />

<.artist_links joinphrase_class="text-sm" artists={@record.artists} />

<.button_group> <.button variant="soft" phx-click={MusicLibraryWeb.Components.Notes.open("record-notes-sheet")} > {gettext("Open Notes")} <.icon name="hero-pencil" class="h-5 w-5" aria-hidden="true" data-slot="icon" /> <.button variant="soft" phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")} > {gettext("Chat about album")} <.icon name="hero-chat-bubble-left-right" class="h-5 w-5" aria-hidden="true" data-slot="icon" /> <.button :if={@record.selected_release_id} variant="soft" phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")} > {gettext("Show Tracks")} <.icon name="hero-numbered-list" class="h-5 w-5" aria-hidden="true" data-slot="icon" /> <.dropdown id={"actions-#{@record.id}"} placement="bottom-end"> <:toggle> <.button variant="soft"> {gettext("Actions")} <.icon name="hero-ellipsis-vertical" class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer" aria-hidden="true" data-slot="icon" /> <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}> <.dropdown_link id={"actions-#{@record.id}-edit"} patch={~p"/collection/#{@record}/show/edit"} > <.icon name="hero-pencil-square" class="h-4 w-4 mr-1 phx-click-loading:animate-bounce" aria-hidden="true" data-slot="icon" /> {gettext("Edit")} <.dropdown_link id={"actions-#{@record.id}-refresh-cover"} phx-click={JS.push("refresh_cover", value: %{id: @record.id})} > <.icon name="hero-photo" class="h-4 w-4 mr-1 phx-click-loading:animate-bounce" aria-hidden="true" data-slot="icon" /> {gettext("Refresh cover")} <.dropdown_link id={"actions-#{@record.id}-refresh-mb-data"} phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})} > <.icon name="hero-arrow-path" class="h-4 w-4 mr-1 phx-click-loading:animate-spin" aria-hidden="true" data-slot="icon" /> {gettext("Refresh MB data")} <.dropdown_link id={"actions-#{@record.id}-populate-genres"} phx-click={JS.push("populate_genres", value: %{id: @record.id})} > <.icon name="hero-sparkles" class="h-4 w-4 mr-1 phx-click-loading:animate-shake" aria-hidden="true" data-slot="icon" /> {gettext("Populate genres")} <.dropdown_link id={"actions-#{@record.id}-regenerate-embeddings"} phx-click={JS.push("regenerate_embeddings", value: %{id: @record.id})} > <.icon name="hero-sparkles" class="h-4 w-4 mr-1 phx-click-loading:animate-shake" aria-hidden="true" data-slot="icon" /> {gettext("Regenerate embeddings")} <.dropdown_link id={"actions-#{@record.id}-extract-colors-fast"} phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :fast})} > <.icon name="hero-paint-brush" class="h-4 w-4 mr-1 phx-click-loading:animate-shake" aria-hidden="true" data-slot="icon" /> {gettext("Extract colors (fast)")} <.dropdown_link id={"actions-#{@record.id}-extract-colors-slow"} phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :slow})} > <.icon name="hero-paint-brush" class="h-4 w-4 mr-1 phx-click-loading:animate-shake" aria-hidden="true" data-slot="icon" /> {gettext("Extract colors (slow)")} <.dropdown_separator /> <.dropdown_link id={"actions-#{@record.id}-delete"} phx-click={JS.push("delete", value: %{id: @record.id})} data-confirm={gettext("Are you sure?")} class="text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!" > <.icon name="hero-trash" class="h-4 w-4 mr-1 phx-click-loading:animate-spin" aria-hidden="true" data-slot="icon" /> {gettext("Delete")}

{@record.title}

<.record_colors record={@record} /> {Records.Record.format_release_date(@record.release_date)} · {format_label( @record.format )} · {type_label(@record.type)}

<.dl_row label={gettext("ID")}>
{@record.id} <.copy_to_clipboard target_id={"record-#{@record.id}"} label={gettext("Copy record ID to clipboard")} />
<.dl_row label={gettext("Genres")}> <.link :for={genre <- @record.genres} class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300" patch={~p"/collection?#{%{query: ~s(genre:"#{genre}")}}"} > {genre} <.dl_row label={gettext("MusicBrainz ID")}>
{@record.musicbrainz_id} <.copy_to_clipboard target_id={"mb-#{@record.musicbrainz_id}"} label={gettext("Copy MusicBrainz ID to clipboard")} />
<.dl_row label={gettext("Purchased on")}> {Records.Record.format_as_date(@record.purchased_at)} <.dl_row label={gettext("Published releases")}>
{Records.Record.release_count(@record)} <.release_list record={@record} />
<.dl_row label={gettext("Collected release")}>
{gettext("No release selected")} <.release_summary :if={@record.selected_release_id} release={Records.Record.selected_release(@record)} /> @record.id} class="hidden" > {@record.selected_release_id} <.copy_to_clipboard :if={@record.selected_release_id} target_id={"record-selected-release-" <> @record.id} label={gettext("Copy record selected release ID to clipboard")} />
<.dl_row :if={Records.Record.included_release_groups_count(@record) > 0} label={gettext("Includes")} >
  • {included_release_group.artists} - {included_release_group.title}
<.dl_row :if={@record_sets != []} label={gettext("Record sets")}>
  • <.link navigate={~p"/record-sets/#{record_set}"} class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300 hover:underline" > {record_set.name} {gettext( "%{collected}/%{total} collected", RecordSet.count_by_status(record_set) )}
<.dl_row label={gettext("Inserted at")}> {Records.Record.format_as_date(@record.inserted_at)} <.dl_row label={gettext("Updated at")}> {Records.Record.format_as_date(@record.updated_at)} <.dl_row label={gettext("Last listened at")}> {localize_scrobbled_at(@last_listened_track.scrobbled_at_uts, @timezone)} {gettext("Never")} 0}> {ngettext("(1 scrobble)", "(%{count} scrobbles)", @play_count)}
<.similar_records similar_records={@similar_records} record_show_path={fn record -> ~p"/collection/#{record}" end} section={:collection} /> <.json_viewer title={gettext("MusicBrainz data")} data={@record.musicbrainz_data} /> <.text_viewer title={gettext("Record Embedding")} data={@embedding_text} /> <.live_component id="release-with-tracks" sheet_id="release-with-tracks-sheet" module={MusicLibraryWeb.Components.Release} record={@record} /> <.live_component id="record-notes" sheet_id="record-notes-sheet" module={MusicLibraryWeb.Components.Notes} entity={:record} musicbrainz_id={@record.musicbrainz_id} /> <.live_component id="record-chat" sheet_id="record-chat-sheet" module={MusicLibraryWeb.Components.Chat} title={@record.title} chat_module={MusicLibrary.RecordChat} chat_context={{@record, @embedding_text}} placeholder={gettext("Ask about this album...")} empty_prompt={gettext("Ask anything about this album")} /> <.structured_modal :if={@live_action == :edit} id="record-modal" on_close={JS.patch(~p"/collection/#{@record}")} > <.live_component module={MusicLibraryWeb.Components.RecordForm} id={@record.id} action={@live_action} show_purchased_at={true} record={@record} patch={~p"/collection/#{@record}"} />
""" end @impl true def mount(%{"id" => record_id}, _session, socket) do if connected?(socket) do Records.subscribe(record_id) end {:ok, socket |> assign(:current_section, :collection) |> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?()) |> assign(:release_with_tracks, nil)} end @impl true def handle_params(%{"id" => id}, _, socket) do record = Records.get_record!(id) last_listened_track = Records.get_last_listened_track(record) play_count = Records.play_count(record) || 0 socket = if record.selected_release_id do socket else socket end record_sets = RecordSets.list_record_sets_for_record(record.id) {:noreply, socket |> assign(:page_title, page_title(socket.assigns.live_action, record)) |> assign(:record, record) |> assign(:last_listened_track, last_listened_track) |> assign(:play_count, play_count) |> assign(:record_sets, record_sets) |> assign_embedding_text() |> assign_similar_records()} end @impl true def handle_event("delete", %{"id" => id}, socket) do record = Records.get_record!(id) {:ok, _} = Records.delete_record(record) {:noreply, push_navigate(socket, to: ~p"/collection")} end def handle_event("refresh_musicbrainz_data", %{"id" => id}, socket) do record = Records.get_record!(id) case Records.refresh_musicbrainz_data(record) do {:ok, updated_record} -> {:noreply, socket |> put_toast(:info, gettext("MusicBrainz data refreshed successfully")) |> assign(:record, updated_record)} {:error, reason} -> {:noreply, socket |> put_toast( :error, gettext("Error refreshing MusicBrainz data") <> "," <> inspect(reason) )} end end def handle_event("populate_genres", %{"id" => id}, socket) do record = Records.get_record!(id) case Records.populate_genres_async(record) do {:ok, _worker} -> {:noreply, socket |> put_toast(:info, gettext("In progress - record will update automatically"))} {:error, reason} -> {:noreply, socket |> put_toast( :error, gettext("Error") <> "," <> inspect(reason) )} end end def handle_event("refresh_cover", %{"id" => id}, socket) do record = Records.get_record!(id) case Records.refresh_cover(record) do {:ok, record} -> {:noreply, socket |> assign(:record, record) |> put_toast(:info, gettext("Cover refreshed successfully"))} {:error, reason} -> {:noreply, socket |> put_toast( :error, gettext("Error refreshing cover") <> "," <> inspect(reason) )} end end def handle_event("extract_colors", %{"id" => id, "method" => method}, socket) do record = Records.get_record!(id) method = String.to_existing_atom(method) case Records.extract_colors_async(record, method) do {:ok, _worker} -> {:noreply, socket |> put_toast(:info, gettext("In progress - record will update automatically"))} {:error, reason} -> {:noreply, socket |> put_toast( :error, gettext("Error") <> "," <> inspect(reason) )} end end def handle_event("regenerate_embeddings", %{"id" => id}, socket) do record = Records.get_record!(id) case Similarity.generate_embedding_async(record) do {:ok, _worker} -> {:noreply, socket |> put_toast(:info, gettext("In progress - record will update automatically"))} {:error, reason} -> {:noreply, socket |> put_toast( :error, gettext("Error") <> "," <> inspect(reason) )} end end @impl true def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, record}}, socket) do {:noreply, socket |> assign(:record, record) |> assign_similar_records() |> assign_embedding_text()} end @impl true def handle_info({:update, record}, socket) do {:noreply, socket |> put_toast(:info, gettext("Record updated in the background")) |> assign(:record, record) |> assign_similar_records() |> assign_embedding_text()} end def page_title(:show, record) do Enum.join( [ Records.Record.artist_names(record), "-", record.title, "·", gettext("Details"), "·", gettext("Collection") ], " " ) end def page_title(action, record) do Enum.join( [ Records.Record.artist_names(record), "-", record.title, "·", title_segment(action), "·", gettext("Collection") ], " " ) end defp title_segment(:show), do: gettext("Show") defp title_segment(:edit), do: gettext("Edit") defp assign_similar_records(socket) do similar_records = Similarity.find_similar(socket.assigns.record.id, limit: 6, scope: :collection) assign(socket, :similar_records, similar_records) end defp assign_embedding_text(socket) do case Similarity.get_embedding_text(socket.assigns.record.id) do {:ok, text} -> assign(socket, :embedding_text, text) {:error, _reason} -> assign(socket, :embedding_text, gettext("Not available")) end end end