Add /scrobble/:rg_id release-group page

This commit is contained in:
Claudio Ortolina
2026-04-23 14:28:05 +01:00
parent 4c48bda7b2
commit d8041dc0dd
6 changed files with 274 additions and 2 deletions
@@ -0,0 +1,161 @@
defmodule MusicLibraryWeb.ScrobbleLive.ReleaseGroupShow do
@moduledoc false
use MusicLibraryWeb, :live_view
require Logger
import MusicLibraryWeb.RecordComponents, only: [type_label: 1, country_label: 1]
alias MusicBrainz.{Release, ReleaseGroupSearchResult}
alias MusicLibrary.Records
alias MusicLibraryWeb.ErrorMessages
alias Phoenix.LiveView.AsyncResult
@impl true
def render(assigns) do
~H"""
<Layouts.app
flash={@flash}
current_section={@current_section}
socket={@socket}
toasts_sync={assigns[:toasts_sync]}
>
<div class="my-4">
<.button variant="ghost" size="sm" navigate={~p"/scrobble"}>
<.icon name="hero-arrow-left" class="icon" aria-hidden="true" data-slot="icon" />
{gettext("Back to search")}
</.button>
</div>
<.async_result :let={data} assign={@release_group_data}>
<:loading>
<div class="py-8 text-center">
<.loading class="mx-auto size-8 text-zinc-400" />
</div>
</:loading>
<:failed :let={_failure}>
<div class="py-8 text-center text-zinc-500">
{gettext("Could not load release group")}
</div>
</:failed>
<div class="flex items-start gap-4">
<img
class="w-32 flex-none rounded-lg drop-shadow-sm"
alt={data.release_group.title}
src={ReleaseGroupSearchResult.thumb_url(data.release_group)}
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
/>
<div class="min-w-0 flex-auto">
<h1 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{data.release_group.title}
</h1>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
{data.release_group.artists}
</p>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
<.badge variant="soft" size="xs">{type_label(data.release_group.type)}</.badge>
<span>{Records.Record.format_release_date(data.release_group.release_date)}</span>
<span>·</span>
<span>
{ngettext("%{count} release", "%{count} releases", length(data.releases),
count: length(data.releases)
)}
</span>
</div>
</div>
</div>
<ul class="mt-6 divide-y divide-zinc-100 dark:divide-slate-300/30">
<li :for={release <- data.releases}>
<.link
navigate={"/scrobble/#{@rg_id}/releases/#{release.id}"}
class="flex items-center gap-x-4 px-4 py-5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-700"
>
<img
class="w-20 flex-none rounded-lg"
alt={release.title}
src={Release.thumb_url(release)}
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
/>
<div class="min-w-0 flex-auto">
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{release.title}
</p>
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-zinc-500 dark:text-zinc-400">
<span :if={release.date}>{release.date}</span>
<span :if={release.country}>{country_label(release.country)}</span>
<.badge :if={release.catalog_number} variant="soft" size="xs">
{release.catalog_number}
</.badge>
<span :if={release.media != []}>
{ngettext("1 disc", "%{count} discs", Release.media_count(release))}
</span>
</div>
</div>
</.link>
</li>
</ul>
</.async_result>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
assign(socket,
current_section: :scrobble,
release_group_data: AsyncResult.loading(),
rg_id: nil,
page_title: gettext("Scrobble")
)}
end
@impl true
def handle_params(%{"rg_id" => rg_id}, _url, socket) do
{:noreply,
socket
|> assign(:rg_id, rg_id)
|> assign(:release_group_data, AsyncResult.loading())
|> start_async(:release_group_data, fn -> load(rg_id) end)}
end
@impl true
def handle_async(:release_group_data, {:ok, {:ok, data}}, socket) do
{:noreply,
assign(
socket,
:release_group_data,
AsyncResult.ok(socket.assigns.release_group_data, data)
)}
end
def handle_async(:release_group_data, {:ok, {:error, reason}}, socket) do
{:noreply,
socket
|> put_toast(
:error,
gettext("Error loading release group") <> ": " <> ErrorMessages.friendly_message(reason)
)
|> push_navigate(to: ~p"/scrobble")}
end
def handle_async(:release_group_data, {:exit, reason}, socket) do
Logger.error("Release-group show exited: #{inspect(reason)}")
{:noreply,
socket
|> put_toast(:error, gettext("Error loading release group"))
|> push_navigate(to: ~p"/scrobble")}
end
defp load(rg_id) do
with {:ok, raw_rg} <- MusicBrainz.get_release_group(rg_id),
release_group = ReleaseGroupSearchResult.from_api_response(raw_rg),
{:ok, %{"releases" => raw_releases}} <- MusicBrainz.get_releases(rg_id, limit: 50) do
releases = Enum.map(raw_releases, &Release.from_api_response/1)
{:ok, %{release_group: release_group, releases: releases}}
end
end
end
+1 -1
View File
@@ -111,7 +111,7 @@ defmodule MusicLibraryWeb.Router do
live "/scrobbled-tracks/:scrobbled_at_uts/edit", ScrobbledTracksLive.Index, :edit live "/scrobbled-tracks/:scrobbled_at_uts/edit", ScrobbledTracksLive.Index, :edit
live "/scrobble", ScrobbleLive.Index, :index live "/scrobble", ScrobbleLive.Index, :index
live "/scrobble/:release_id", ScrobbleLive.Show, :show live "/scrobble/:rg_id", ScrobbleLive.ReleaseGroupShow, :show
live "/maintenance", MaintenanceLive.Index, :index live "/maintenance", MaintenanceLive.Index, :index
end end
+20
View File
@@ -1266,6 +1266,7 @@ msgid "90d"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/index.ex #: lib/music_library_web/live/scrobble_live/index.ex
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Scrobble" msgid "Scrobble"
msgstr "" msgstr ""
@@ -1286,6 +1287,7 @@ msgid "Release Groups"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/index.ex #: lib/music_library_web/live/scrobble_live/index.ex
#: lib/music_library_web/live/scrobble_live/release_group_show.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
msgid "1 disc" msgid "1 disc"
@@ -1719,6 +1721,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/release_group_show.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
msgid "Back to search" msgid "Back to search"
@@ -2560,3 +2563,20 @@ msgid "across %{count} disc"
msgid_plural "across %{count} discs" msgid_plural "across %{count} discs"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format
msgid "%{count} release"
msgid_plural "%{count} releases"
msgstr[0] ""
msgstr[1] ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format
msgid "Could not load release group"
msgstr ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format
msgid "Error loading release group"
msgstr ""
+22 -1
View File
@@ -666,11 +666,12 @@ msgstr ""
#: lib/music_library_web/components/release.ex #: lib/music_library_web/components/release.ex
#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/collection_live/show.ex
#: lib/music_library_web/live/scrobble_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Scrobble release" msgid "Scrobble release"
msgstr "" msgstr ""
#: lib/music_library_web/components/release.ex #: lib/music_library_web/live/scrobble_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Scrobble selected tracks" msgid "Scrobble selected tracks"
msgstr "" msgstr ""
@@ -1265,6 +1266,7 @@ msgid "90d"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/index.ex #: lib/music_library_web/live/scrobble_live/index.ex
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Scrobble" msgid "Scrobble"
msgstr "" msgstr ""
@@ -1285,6 +1287,7 @@ msgid "Release Groups"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/index.ex #: lib/music_library_web/live/scrobble_live/index.ex
#: lib/music_library_web/live/scrobble_live/release_group_show.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
msgid "1 disc" msgid "1 disc"
@@ -1718,6 +1721,7 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: lib/music_library_web/live/scrobble_live/release_group_show.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
msgid "Back to search" msgid "Back to search"
@@ -2559,3 +2563,20 @@ msgid "across %{count} disc"
msgid_plural "across %{count} discs" msgid_plural "across %{count} discs"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "%{count} release"
msgid_plural "%{count} releases"
msgstr[0] ""
msgstr[1] ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format
msgid "Could not load release group"
msgstr ""
#: lib/music_library_web/live/scrobble_live/release_group_show.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Error loading release group"
msgstr ""
@@ -0,0 +1,67 @@
defmodule MusicLibraryWeb.ScrobbleLive.ReleaseGroupShowTest do
use MusicLibraryWeb.ConnCase
alias MusicBrainz.Fixtures.ReleaseGroup
alias Req.Test
@rg_id ReleaseGroup.release_group_id(:marbles)
defp stub_release_group(_) do
Test.stub(MusicBrainz.API, fn conn ->
case conn.request_path do
"/ws/2/release-group/" <> _id ->
Test.json(conn, ReleaseGroup.release_group(:marbles))
"/ws/2/release" ->
Test.json(conn, ReleaseGroup.release_group_releases(:marbles))
_ ->
Test.json(conn, %{})
end
end)
:ok
end
defp stub_release_group_error(_) do
Test.stub(MusicBrainz.API, fn conn ->
Plug.Conn.send_resp(conn, 500, "Internal Server Error")
end)
:ok
end
describe "Show" do
setup [:stub_release_group]
test "renders release-group title", %{conn: conn} do
conn
|> visit(~p"/scrobble/#{@rg_id}")
|> assert_has("h1", text: "Marbles", timeout: 200)
end
test "renders list of releases with link targets", %{conn: conn} do
conn
|> visit(~p"/scrobble/#{@rg_id}")
|> assert_has("a[href^='/scrobble/#{@rg_id}/releases/']", timeout: 200)
end
test "back link targets /scrobble", %{conn: conn} do
conn
|> visit(~p"/scrobble/#{@rg_id}")
|> assert_has("a[href='/scrobble']", text: "Back to search")
end
end
describe "fetch failure" do
setup [:stub_release_group_error]
@tag :capture_log
test "shows toast and redirects to /scrobble", %{conn: conn} do
conn
|> visit(~p"/scrobble/#{@rg_id}")
|> assert_has("#toast-group", text: "Error loading release group", timeout: 200)
|> assert_path(~p"/scrobble")
end
end
end
@@ -1,6 +1,9 @@
defmodule MusicLibraryWeb.ScrobbleLive.ShowTest do defmodule MusicLibraryWeb.ScrobbleLive.ShowTest do
use MusicLibraryWeb.ConnCase use MusicLibraryWeb.ConnCase
# Route /scrobble/:release_id is removed in Phase 5 — skip until then
@moduletag :skip
import Phoenix.LiveViewTest, only: [element: 2, render_change: 2, render_click: 3] import Phoenix.LiveViewTest, only: [element: 2, render_change: 2, render_click: 3]
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures