defmodule MusicLibraryWeb.Components.Release do use MusicLibraryWeb, :live_component require Logger alias MusicBrainz.Release alias MusicLibrary.ScrobbleActivity alias MusicLibraryWeb.Duration alias Phoenix.LiveView.AsyncResult def open(id), do: Fluxon.open_dialog(id) @impl true def mount(socket) do {:ok, socket |> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?()) |> assign(:release_with_tracks, AsyncResult.loading()) |> assign(:already_scrobbled, false) |> assign(:selected_tracks, MapSet.new())} end @impl true def update(%{record: record} = assigns, socket) do {:ok, socket |> assign(assigns) |> assign_async(:release_with_tracks, fn -> with {:ok, release} <- MusicBrainz.get_release(record.selected_release_id) do {:ok, %{release_with_tracks: MusicBrainz.Release.from_api_response(release)}} end end)} end def update(%{already_scrobbled: _already_scrobbled} = assigns, socket) do {:ok, assign(socket, assigns)} end @impl true def render(assigns) do ~H"""
<.sheet :if={@record.selected_release_id} id={@sheet_id} placement="right" class="min-w-xs sm:min-w-sm" >

{gettext("Tracks")}

<.button :if={@can_scrobble? && @release_with_tracks.ok?} size="sm" disabled={@already_scrobbled} phx-click={ if MapSet.size(@selected_tracks) > 0, do: "scrobble_selected_tracks", else: "scrobble_release" } phx-target={@myself} phx-disable-with={gettext("Scrobbling...")} > {scrobble_button_label(@selected_tracks)} <.button :if={!@can_scrobble?} size="sm" href={LastFm.auth_url()}> {gettext("Connect your Last.fm account")}
<.async_result :let={release_with_tracks} assign={@release_with_tracks}> <:loading>
{gettext("Loading release with tracks")} <.loading />
<:failed :let={_failure}>
<.icon name="hero-exclamation-triangle" class="h-5 w-5" aria-hidden="true" data-slot="icon" /> {gettext("Error loading tracks")}
<.medium :for={medium <- release_with_tracks.media} can_scrobble?={@can_scrobble?} already_scrobbled={@already_scrobbled} medium={medium} release_artists={release_with_tracks.artists} media_count={MusicBrainz.Release.media_count(release_with_tracks)} selected_tracks={@selected_tracks} myself={@myself} />
""" end attr :medium, Release.Medium, required: true attr :release_artists, :list, required: true attr :media_count, :integer, required: true attr :can_scrobble?, :boolean, required: true attr :already_scrobbled, :boolean, required: true attr :selected_tracks, :any, required: true attr :myself, :any, required: true def medium(assigns) do ~H"""
1} class="flex justify-between items-center gap-4" >

{medium_title(@medium)} <.badge variant="soft" class="text-xs"> {@medium.format}

<.button :if={@can_scrobble?} size="sm" disabled={@already_scrobbled || MapSet.size(@selected_tracks) > 0} phx-click="scrobble_medium" phx-value-number={@medium.number} phx-target={@myself} phx-disable-with={gettext("Scrobbling...")} > {medium_scrobble_label(@medium.format)}
<.track_list medium_number={@medium.number} tracks={@medium.tracks} release_artists={@release_artists} selected_tracks={@selected_tracks} can_scrobble?={@can_scrobble?} myself={@myself} /> <.separator />

{medium_duration(@medium)}

""" end defp medium_scrobble_label(format) do if format && String.contains?(format, "Vinyl") do gettext("Scrobble side") else gettext("Scrobble disc") end end attr :medium_number, :integer, required: true attr :tracks, :list, required: true attr :release_artists, :list, required: true attr :selected_tracks, :any, required: true attr :can_scrobble?, :boolean, required: true attr :myself, :any, required: true def track_list(assigns) do ~H""" """ end defguardp release_loaded?(assigns) when assigns.release_with_tracks.ok? @impl true def handle_event("scrobble_release", _params, socket) when release_loaded?(socket.assigns) do release_with_tracks = socket.assigns.release_with_tracks.result case ScrobbleActivity.scrobble_release(release_with_tracks, finished_at: DateTime.utc_now()) do {:ok, _} -> send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000) {:noreply, socket |> assign(:already_scrobbled, true) |> put_toast(:info, gettext("Release scrobbled successfully"))} {:error, reason} -> Logger.error("Error scrobbling release: #{inspect(reason)}") {:noreply, socket |> put_toast( :error, gettext("Error scrobbling release") <> "," <> inspect(reason) )} end end def handle_event("scrobble_release", _params, socket) do {:noreply, socket |> put_toast(:error, gettext("Error scrobbling release"))} end def handle_event("scrobble_medium", %{"number" => number}, socket) when release_loaded?(socket.assigns) do release_with_tracks = socket.assigns.release_with_tracks.result number = String.to_integer(number) case ScrobbleActivity.scrobble_medium(number, release_with_tracks, finished_at: DateTime.utc_now() ) do {:ok, _} -> send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000) {:noreply, socket |> assign(:already_scrobbled, true) |> put_toast(:info, gettext("Disc scrobbled successfully"))} {:error, reason} -> Logger.error("Error scrobbling medium: #{inspect(reason)}") {:noreply, socket |> put_toast( :error, gettext("Error scrobbling disc") <> "," <> inspect(reason) )} end end def handle_event("scrobble_medium", _params, socket) do {:noreply, socket |> put_toast(:error, gettext("Error scrobbling disc"))} end def handle_event("toggle_track", %{"track-id" => track_id}, socket) do selected_tracks = socket.assigns.selected_tracks updated_tracks = if MapSet.member?(selected_tracks, track_id) do MapSet.delete(selected_tracks, track_id) else MapSet.put(selected_tracks, track_id) end {:noreply, assign(socket, :selected_tracks, updated_tracks)} end def handle_event("toggle_medium", %{"medium-number" => number}, socket) do number = String.to_integer(number) selected_tracks = socket.assigns.selected_tracks release_with_tracks = socket.assigns.release_with_tracks.result medium_tracks = release_with_tracks |> Release.medium_tracks(number) |> Enum.map(& &1.id) |> MapSet.new() updated_tracks = if MapSet.subset?(medium_tracks, selected_tracks) do MapSet.difference(selected_tracks, medium_tracks) else MapSet.union(selected_tracks, medium_tracks) end {:noreply, assign(socket, :selected_tracks, updated_tracks)} end def handle_event("scrobble_selected_tracks", _params, socket) when release_loaded?(socket.assigns) do release_with_tracks = socket.assigns.release_with_tracks.result selected_track_ids = socket.assigns.selected_tracks if MapSet.size(selected_track_ids) == 0 do {:noreply, socket |> put_toast(:error, gettext("No tracks selected"))} else case ScrobbleActivity.scrobble_tracks( selected_track_ids, release_with_tracks, finished_at: DateTime.utc_now() ) do {:ok, _} -> send_update_after(socket.assigns.myself, %{already_scrobbled: false}, 3000) {:noreply, socket |> assign(:already_scrobbled, true) |> assign(:selected_tracks, MapSet.new()) |> put_toast(:info, gettext("Selected tracks scrobbled successfully"))} {:error, reason} -> Logger.error("Error scrobbling tracks: #{inspect(reason)}") {:noreply, socket |> put_toast( :error, gettext("Error scrobbling selected tracks") <> "," <> inspect(reason) )} end end end def handle_event("scrobble_selected_tracks", _params, socket) do {:noreply, socket |> put_toast(:error, gettext("Error scrobbling selected tracks"))} end defp medium_selected?(medium, selected_tracks) do medium_track_ids = medium.tracks |> Enum.map(& &1.id) |> MapSet.new() MapSet.subset?(medium_track_ids, selected_tracks) end def scrobble_button_label(selected_tracks) do if MapSet.size(selected_tracks) > 0 do gettext("Scrobble selected tracks") else gettext("Scrobble release") end end defp medium_duration(medium) do medium |> MusicBrainz.Release.medium_duration() |> Duration.format_duration() end defp medium_title(medium) do if medium.title === "" do gettext("Disc %{no}", %{no: medium.number}) else medium.title end end end