diff --git a/lib/music_library/records/tracklist_pdf.ex b/lib/music_library/records/tracklist_pdf.ex index 22594e2b..8be20fc2 100644 --- a/lib/music_library/records/tracklist_pdf.ex +++ b/lib/music_library/records/tracklist_pdf.ex @@ -16,19 +16,28 @@ defmodule MusicLibrary.Records.TracklistPdf do @spec generate(MusicLibrary.Records.Record.t(), Release.t()) :: {:ok, binary()} | {:error, term()} def generate(record, release) do - markup = build_markup(record, release) + markup = build_markup(record, release.media) Typst.render_to_pdf(markup) end - defp build_markup(record, release) do - media_count = Release.media_count(release) - track_count = Enum.sum(Enum.map(release.media, &length(&1.tracks))) + @spec generate_medium(MusicLibrary.Records.Record.t(), Release.t(), integer()) :: + {:ok, binary()} | {:error, term()} + def generate_medium(record, release, medium_number) do + case Release.get_medium(release, medium_number) do + nil -> {:error, :medium_not_found} + medium -> generate(record, %{release | media: [medium]}) + end + end + + defp build_markup(record, media) do + media_count = length(media) + track_count = Enum.sum(Enum.map(media, &length(&1.tracks))) header_count = if media_count > 1, do: media_count, else: 0 total_items = track_count + header_count {columns, font_size, show_duration} = layout_params(total_items, media_count) - content = build_media(release, media_count, font_size, show_duration) + content = build_media(media, media_count, font_size, show_duration) """ #set page(width: 120mm, height: 120mm, margin: 5mm, background: rect(width: 100%, height: 100%, stroke: 0.5pt + black)) @@ -56,8 +65,8 @@ defmodule MusicLibrary.Records.TracklistPdf do """ end - defp build_media(release, media_count, font_size, show_duration) do - release.media + defp build_media(media, media_count, font_size, show_duration) do + media |> Enum.map_join("\n", fn medium -> build_medium(medium, media_count, font_size, show_duration) end) diff --git a/lib/music_library_web/components/release.ex b/lib/music_library_web/components/release.ex index 48891546..1944ddf2 100644 --- a/lib/music_library_web/components/release.ex +++ b/lib/music_library_web/components/release.ex @@ -136,6 +136,7 @@ defmodule MusicLibraryWeb.Components.Release do media_count={MusicBrainz.Release.media_count(release_with_tracks)} selected_tracks={@selected_tracks} myself={@myself} + record={@record} /> @@ -151,6 +152,7 @@ defmodule MusicLibraryWeb.Components.Release do attr :already_scrobbled, :boolean, required: true attr :selected_tracks, :any, required: true attr :myself, :any, required: true + attr :record, :any, default: nil def medium(assigns) do ~H""" @@ -174,17 +176,29 @@ defmodule MusicLibraryWeb.Components.Release do {@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)} - +
+ <.button + :if={@record} + variant="ghost" + size="sm" + phx-click="print_medium_tracklist" + phx-value-medium-number={@medium.number} + phx-target={@myself} + > + <.icon name="hero-printer" class="h-4 w-4" /> + + <.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} @@ -431,6 +445,40 @@ defmodule MusicLibraryWeb.Components.Release do {:noreply, socket} end + def handle_event("print_medium_tracklist", %{"medium-number" => number}, socket) + when release_loaded?(socket.assigns) do + release = socket.assigns.release_with_tracks.result + record = socket.assigns.record + {number, ""} = Integer.parse(number) + + case TracklistPdf.generate_medium(record, release, number) do + {:ok, pdf_binary} -> + filename = "#{record.title} - Disc #{number} - Tracklist.pdf" + + {:noreply, + push_event(socket, "music_library:download", %{ + data: Base.encode64(pdf_binary), + filename: filename, + content_type: "application/pdf" + })} + + {:error, reason} -> + Logger.error("Error generating medium tracklist PDF: #{inspect(reason)}") + + put_toast!( + :error, + gettext("Error generating tracklist PDF") <> + ": " <> ErrorMessages.friendly_message(reason) + ) + + {:noreply, socket} + end + end + + def handle_event("print_medium_tracklist", _params, socket) do + {:noreply, socket} + end + def handle_event("load_release_tracks", _params, socket) do selected_release_id = socket.assigns.record.selected_release_id diff --git a/test/music_library/records/tracklist_pdf_test.exs b/test/music_library/records/tracklist_pdf_test.exs index ad5ecd25..68f1bc99 100644 --- a/test/music_library/records/tracklist_pdf_test.exs +++ b/test/music_library/records/tracklist_pdf_test.exs @@ -80,6 +80,28 @@ defmodule MusicLibrary.Records.TracklistPdfTest do end end + describe "generate_medium/3" do + test "generates PDF for a single medium from a multi-disc release" do + record = build_record(%{title: "Marbles", artists: [%{name: "Marillion"}]}) + api_response = MusicBrainz.Fixtures.Release.release_with_media(:marbles) + release = Release.from_api_response(api_response) + + assert {:ok, <<@pdf_magic_bytes, _::binary>>} = + TracklistPdf.generate_medium(record, release, 1) + end + + test "returns error for non-existent medium number" do + record = build_record(%{title: "Test Album", artists: [%{name: "Test Artist"}]}) + + release = + build_release([ + build_medium(1, [build_track(1, "Track One", 200_000)]) + ]) + + assert {:error, :medium_not_found} = TracklistPdf.generate_medium(record, release, 99) + end + end + describe "layout_params/2" do test "single column for small track count" do assert {1, 8, true} = TracklistPdf.layout_params(10, 1)