Add track selection and group scrobbling to release sheet (#62)
* Initial plan * Add track selection and scrobble functionality Co-authored-by: cloud8421 <537608+cloud8421@users.noreply.github.com> * Add translations for track selection feature Co-authored-by: cloud8421 <537608+cloud8421@users.noreply.github.com> * Format code * Temporarily set selected tracks in scrobble anything view --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cloud8421 <537608+cloud8421@users.noreply.github.com> Co-authored-by: Claudio Ortolina <cloud8421@gmail.com>
This commit is contained in:
@@ -97,6 +97,54 @@ defmodule MusicLibrary.ScrobbleActivity do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def scrobble_tracks(selected_track_ids, release_with_tracks, opts) when is_list(opts) do
|
||||||
|
case Enum.sort(opts) do
|
||||||
|
[finished_at: _, started_at: _] ->
|
||||||
|
raise ArgumentError, """
|
||||||
|
Cannot scrobble tracks with both started_at and finished_at.
|
||||||
|
Remove either of them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[started_at: started_at] ->
|
||||||
|
scrobble_tracks(selected_track_ids, release_with_tracks, {:started_at, started_at})
|
||||||
|
|
||||||
|
[finished_at: finished_at] ->
|
||||||
|
scrobble_tracks(selected_track_ids, release_with_tracks, {:finished_at, finished_at})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def scrobble_tracks(selected_track_ids, release_with_tracks, {:finished_at, finished_at}) do
|
||||||
|
all_tracks = Release.tracks(release_with_tracks)
|
||||||
|
|
||||||
|
selected_tracks =
|
||||||
|
Enum.filter(all_tracks, fn track -> MapSet.member?(selected_track_ids, track.id) end)
|
||||||
|
|
||||||
|
tracks_duration = Enum.sum_by(selected_tracks, fn track -> track.length || 0 end)
|
||||||
|
started_at = DateTime.add(finished_at, -tracks_duration, :millisecond)
|
||||||
|
scrobble_tracks(selected_track_ids, release_with_tracks, {:started_at, started_at})
|
||||||
|
end
|
||||||
|
|
||||||
|
def scrobble_tracks(selected_track_ids, release_with_tracks, {:started_at, started_at}) do
|
||||||
|
all_tracks = Release.tracks(release_with_tracks)
|
||||||
|
|
||||||
|
selected_tracks =
|
||||||
|
Enum.filter(all_tracks, fn track -> MapSet.member?(selected_track_ids, track.id) end)
|
||||||
|
|
||||||
|
tracks_duration = Enum.sum_by(selected_tracks, fn track -> track.length || 0 end)
|
||||||
|
|
||||||
|
if tracks_duration == 0 do
|
||||||
|
{:error, :no_duration}
|
||||||
|
else
|
||||||
|
session_key = Secrets.get!("last_fm_session_key").value
|
||||||
|
|
||||||
|
{scrobbles, _finished_at} =
|
||||||
|
selected_tracks
|
||||||
|
|> to_scrobbles(release_with_tracks, started_at)
|
||||||
|
|
||||||
|
LastFm.scrobble(scrobbles, session_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp to_scrobbles(tracks, release_with_tracks, started_at) do
|
defp to_scrobbles(tracks, release_with_tracks, started_at) do
|
||||||
tracks
|
tracks
|
||||||
|> Enum.map_reduce(started_at, fn track, time ->
|
|> Enum.map_reduce(started_at, fn track, time ->
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
socket
|
socket
|
||||||
|> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?())
|
|> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?())
|
||||||
|> assign(:release_with_tracks, AsyncResult.loading())
|
|> assign(:release_with_tracks, AsyncResult.loading())
|
||||||
|> assign(:already_scrobbled, false)}
|
|> assign(:already_scrobbled, false)
|
||||||
|
|> assign(:selected_tracks, MapSet.new())}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -49,11 +50,15 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
:if={@can_scrobble? && @release_with_tracks.ok?}
|
:if={@can_scrobble? && @release_with_tracks.ok?}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={@already_scrobbled}
|
disabled={@already_scrobbled}
|
||||||
phx-click="scrobble_release"
|
phx-click={
|
||||||
|
if MapSet.size(@selected_tracks) > 0,
|
||||||
|
do: "scrobble_selected_tracks",
|
||||||
|
else: "scrobble_release"
|
||||||
|
}
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
phx-disable-with={gettext("Scrobbling...")}
|
phx-disable-with={gettext("Scrobbling...")}
|
||||||
>
|
>
|
||||||
{gettext("Scrobble release")}
|
{scrobble_button_label(@selected_tracks)}
|
||||||
</.button>
|
</.button>
|
||||||
<.button :if={!@can_scrobble?} size="sm" href={LastFm.auth_url()}>
|
<.button :if={!@can_scrobble?} size="sm" href={LastFm.auth_url()}>
|
||||||
{gettext("Connect your Last.fm account")}
|
{gettext("Connect your Last.fm account")}
|
||||||
@@ -86,6 +91,7 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
medium={medium}
|
medium={medium}
|
||||||
release_artists={release_with_tracks.artists}
|
release_artists={release_with_tracks.artists}
|
||||||
media_count={MusicBrainz.Release.media_count(release_with_tracks)}
|
media_count={MusicBrainz.Release.media_count(release_with_tracks)}
|
||||||
|
selected_tracks={@selected_tracks}
|
||||||
myself={@myself}
|
myself={@myself}
|
||||||
/>
|
/>
|
||||||
</.async_result>
|
</.async_result>
|
||||||
@@ -100,6 +106,7 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
attr :media_count, :integer, required: true
|
attr :media_count, :integer, required: true
|
||||||
attr :can_scrobble?, :boolean, required: true
|
attr :can_scrobble?, :boolean, required: true
|
||||||
attr :already_scrobbled, :boolean, required: true
|
attr :already_scrobbled, :boolean, required: true
|
||||||
|
attr :selected_tracks, :any, required: true
|
||||||
attr :myself, :any, required: true
|
attr :myself, :any, required: true
|
||||||
|
|
||||||
def medium(assigns) do
|
def medium(assigns) do
|
||||||
@@ -127,6 +134,9 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
medium_number={@medium.number}
|
medium_number={@medium.number}
|
||||||
tracks={@medium.tracks}
|
tracks={@medium.tracks}
|
||||||
release_artists={@release_artists}
|
release_artists={@release_artists}
|
||||||
|
selected_tracks={@selected_tracks}
|
||||||
|
can_scrobble?={@can_scrobble?}
|
||||||
|
myself={@myself}
|
||||||
/>
|
/>
|
||||||
<.separator />
|
<.separator />
|
||||||
<p class="text-xs md:text-sm text-right text-zinc-700 dark:text-zinc-300">
|
<p class="text-xs md:text-sm text-right text-zinc-700 dark:text-zinc-300">
|
||||||
@@ -146,6 +156,9 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
attr :medium_number, :integer, required: true
|
attr :medium_number, :integer, required: true
|
||||||
attr :tracks, :list, required: true
|
attr :tracks, :list, required: true
|
||||||
attr :release_artists, :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
|
def track_list(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@@ -155,6 +168,17 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
class="contents leading-5 text-zinc-700 dark:text-zinc-300 list-none"
|
class="contents leading-5 text-zinc-700 dark:text-zinc-300 list-none"
|
||||||
>
|
>
|
||||||
<div class="table-row">
|
<div class="table-row">
|
||||||
|
<span :if={@can_scrobble?} class="table-cell pr-2 align-middle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={"track-checkbox-#{track.id}"}
|
||||||
|
checked={MapSet.member?(@selected_tracks, track.id)}
|
||||||
|
phx-click="toggle_track"
|
||||||
|
phx-value-track-id={track.id}
|
||||||
|
phx-target={@myself}
|
||||||
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<span class="table-cell text-xs text-right pr-1 text-nowrap">
|
<span class="table-cell text-xs text-right pr-1 text-nowrap">
|
||||||
{track.number || track.position}
|
{track.number || track.position}
|
||||||
</span>
|
</span>
|
||||||
@@ -169,6 +193,7 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
:if={@release_artists !== track.artists}
|
:if={@release_artists !== track.artists}
|
||||||
class="table-row text-xs md:text-sm"
|
class="table-row text-xs md:text-sm"
|
||||||
>
|
>
|
||||||
|
<span :if={@can_scrobble?} class="table-cell" />
|
||||||
<span class="table-cell" />
|
<span class="table-cell" />
|
||||||
<span class="table-cell">
|
<span class="table-cell">
|
||||||
{Enum.map_join(track.artists, ", ", fn artist -> artist.name end)}
|
{Enum.map_join(track.artists, ", ", fn artist -> artist.name end)}
|
||||||
@@ -238,6 +263,56 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
{:noreply, socket |> put_toast(:error, gettext("Error scrobbling disc"))}
|
{:noreply, socket |> put_toast(:error, gettext("Error scrobbling disc"))}
|
||||||
end
|
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("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} ->
|
||||||
|
{: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_duration(medium) do
|
defp medium_duration(medium) do
|
||||||
medium
|
medium
|
||||||
|> MusicBrainz.Release.medium_duration()
|
|> MusicBrainz.Release.medium_duration()
|
||||||
@@ -251,4 +326,12 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
medium.title
|
medium.title
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp scrobble_button_label(selected_tracks) do
|
||||||
|
if MapSet.size(selected_tracks) > 0 do
|
||||||
|
gettext("Scrobble selected tracks")
|
||||||
|
else
|
||||||
|
gettext("Scrobble release")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
already_scrobbled={false}
|
already_scrobbled={false}
|
||||||
medium={medium}
|
medium={medium}
|
||||||
release_artists={@release.artists}
|
release_artists={@release.artists}
|
||||||
|
selected_tracks={MapSet.new()}
|
||||||
media_count={MusicBrainz.Release.media_count(@release)}
|
media_count={MusicBrainz.Release.media_count(@release)}
|
||||||
myself={nil}
|
myself={nil}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -766,6 +766,26 @@ msgstr ""
|
|||||||
msgid "Scrobble release"
|
msgid "Scrobble release"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Scrobble selected tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Selected tracks scrobbled successfully"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Error scrobbling selected tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No tracks selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/release.ex
|
#: lib/music_library_web/components/release.ex
|
||||||
#: lib/music_library_web/live/scrobble_live/show.ex
|
#: lib/music_library_web/live/scrobble_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|||||||
@@ -766,6 +766,26 @@ msgstr ""
|
|||||||
msgid "Scrobble release"
|
msgid "Scrobble release"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Scrobble selected tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Selected tracks scrobbled successfully"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Error scrobbling selected tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "No tracks selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/release.ex
|
#: lib/music_library_web/components/release.ex
|
||||||
#: lib/music_library_web/live/scrobble_live/show.ex
|
#: lib/music_library_web/live/scrobble_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|||||||
Reference in New Issue
Block a user