Refactor TracklistPdf to take release alone

This commit is contained in:
Claudio Ortolina
2026-04-23 07:48:53 +01:00
parent 8631b97b4d
commit 0d1f580379
3 changed files with 81 additions and 94 deletions
+12 -14
View File
@@ -1,6 +1,6 @@
defmodule MusicLibrary.Records.TracklistPdf do defmodule MusicLibrary.Records.TracklistPdf do
@moduledoc """ @moduledoc """
Generates 120mm x 120mm PDF tracklists from record and release data via Typst. Generates 120mm x 120mm PDF tracklists from release data via Typst.
""" """
alias MusicBrainz.Release alias MusicBrainz.Release
@@ -21,23 +21,21 @@ defmodule MusicLibrary.Records.TracklistPdf do
# including the header area consumed by artist name and album title. # including the header area consumed by artist name and album title.
@capacities %{8 => 35, 7 => 38, 6 => 45, 5 => 53} @capacities %{8 => 35, 7 => 38, 6 => 45, 5 => 53}
@spec generate(MusicLibrary.Records.Record.t(), Release.t()) :: @spec generate(Release.t()) :: {:ok, binary()} | {:error, term()}
{:ok, binary()} | {:error, term()} def generate(release) do
def generate(record, release) do markup = build_markup(release, release.media)
markup = build_markup(record, release.media)
Typst.render_to_pdf(markup) Typst.render_to_pdf(markup)
end end
@spec generate_medium(MusicLibrary.Records.Record.t(), Release.t(), integer()) :: @spec generate_medium(Release.t(), integer()) :: {:ok, binary()} | {:error, term()}
{:ok, binary()} | {:error, term()} def generate_medium(release, medium_number) do
def generate_medium(record, release, medium_number) do
case Release.get_medium(release, medium_number) do case Release.get_medium(release, medium_number) do
nil -> {:error, :medium_not_found} nil -> {:error, :medium_not_found}
medium -> generate(record, %{release | media: [medium]}) medium -> generate(%{release | media: [medium]})
end end
end end
defp build_markup(record, media) do defp build_markup(release, media) do
media_count = length(media) media_count = length(media)
track_count = Enum.sum(Enum.map(media, &length(&1.tracks))) track_count = Enum.sum(Enum.map(media, &length(&1.tracks)))
header_count = if media_count > 1, do: media_count, else: 0 header_count = if media_count > 1, do: media_count, else: 0
@@ -54,9 +52,9 @@ defmodule MusicLibrary.Records.TracklistPdf do
#place(top + center, scope: "parent", float: true)[ #place(top + center, scope: "parent", float: true)[
#align(center)[ #align(center)[
#text(size: 10pt, weight: "bold")[#{Format.escape(artist_names(record))}] #text(size: 10pt, weight: "bold")[#{Format.escape(artist_names(release))}]
#linebreak() #linebreak()
#text(size: 9pt, style: "italic")[#{Format.escape(record.title)}] #text(size: 9pt, style: "italic")[#{Format.escape(release.title)}]
] ]
#v(3mm) #v(3mm)
] ]
@@ -138,8 +136,8 @@ defmodule MusicLibrary.Records.TracklistPdf do
Map.fetch!(@capacities, font_size) * columns Map.fetch!(@capacities, font_size) * columns
end end
defp artist_names(record) do defp artist_names(release) do
Enum.map_join(record.artists, fn artist -> Enum.map_join(release.artists, fn artist ->
artist.name <> (artist.joinphrase || "") artist.name <> (artist.joinphrase || "")
end) end)
end end
+2 -2
View File
@@ -620,7 +620,7 @@ defmodule MusicLibraryWeb.Components.Release do
release = socket.assigns.release_with_tracks.result release = socket.assigns.release_with_tracks.result
record = socket.assigns.record record = socket.assigns.record
case TracklistPdf.generate(record, release) do case TracklistPdf.generate(release) do
{:ok, pdf_binary} -> {:ok, pdf_binary} ->
filename = "#{record.title} - Tracklist.pdf" filename = "#{record.title} - Tracklist.pdf"
@@ -654,7 +654,7 @@ defmodule MusicLibraryWeb.Components.Release do
record = socket.assigns.record record = socket.assigns.record
{number, ""} = Integer.parse(number) {number, ""} = Integer.parse(number)
case TracklistPdf.generate_medium(record, release, number) do case TracklistPdf.generate_medium(release, number) do
{:ok, pdf_binary} -> {:ok, pdf_binary} ->
filename = "#{record.title} - Disc #{number} - Tracklist.pdf" filename = "#{record.title} - Disc #{number} - Tracklist.pdf"
@@ -2,110 +2,106 @@ defmodule MusicLibrary.Records.TracklistPdfTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias MusicBrainz.Release alias MusicBrainz.Release
alias MusicLibrary.Records.{Record, TracklistPdf} alias MusicLibrary.Records.TracklistPdf
@pdf_magic_bytes <<37, 80, 68, 70>> @pdf_magic_bytes <<37, 80, 68, 70>>
describe "generate/2" do describe "generate/1" do
test "generates PDF for single-disc release" do test "generates PDF for single-disc release" do
record = build_record(%{title: "OK Computer", artists: [%{name: "Radiohead"}]})
release = release =
build_release([ build_release(
build_medium(1, [ [
build_track(1, "Airbag", 284_533), build_medium(1, [
build_track(2, "Paranoid Android", 383_000) build_track(1, "Airbag", 284_533),
]) build_track(2, "Paranoid Android", 383_000)
]) ])
],
title: "OK Computer",
artists: [%{name: "Radiohead"}]
)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release) assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "generates PDF for multi-disc release" do test "generates PDF for multi-disc release" do
record = build_record(%{title: "Marbles", artists: [%{name: "Marillion"}]})
api_response = MusicBrainz.Fixtures.Release.release_with_media(:marbles) api_response = MusicBrainz.Fixtures.Release.release_with_media(:marbles)
release = Release.from_api_response(api_response) release = Release.from_api_response(api_response)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release) assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "renders artist joinphrase correctly" do test "renders artist joinphrase correctly" do
record = release =
build_record(%{ build_release(
[
build_medium(1, [build_track(1, "Track One", 200_000)])
],
title: "Collab Album", title: "Collab Album",
artists: [ artists: [
%{name: "Artist A", joinphrase: " & "}, %{name: "Artist A", joinphrase: " & "},
%{name: "Artist B"} %{name: "Artist B"}
] ]
}) )
release = assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
build_release([
build_medium(1, [build_track(1, "Track One", 200_000)])
])
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "handles track without duration" do test "handles track without duration" do
record = build_record(%{title: "Test Album", artists: [%{name: "Test Artist"}]})
release = release =
build_release([ build_release(
build_medium(1, [ [
build_track(1, "Has Duration", 180_000), build_medium(1, [
build_track(2, "No Duration", nil) build_track(1, "Has Duration", 180_000),
]) build_track(2, "No Duration", nil)
]) ])
],
title: "Test Album",
artists: [%{name: "Test Artist"}]
)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release) assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "handles special characters in title" do test "handles special characters in title" do
record = release =
build_record(%{ build_release(
[
build_medium(1, [
build_track(1, "Track *starring* @someone", 200_000),
build_track(2, "#Hashtag Title", 180_000)
])
],
title: "Album *with* #special @chars", title: "Album *with* #special @chars",
artists: [%{name: "Artist #1"}] artists: [%{name: "Artist #1"}]
}) )
release = assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
build_release([
build_medium(1, [
build_track(1, "Track *starring* @someone", 200_000),
build_track(2, "#Hashtag Title", 180_000)
])
])
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
end end
describe "generate_medium/3" do describe "generate_medium/2" do
test "generates PDF for a single medium from a multi-disc release" 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) api_response = MusicBrainz.Fixtures.Release.release_with_media(:marbles)
release = Release.from_api_response(api_response) release = Release.from_api_response(api_response)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} =
TracklistPdf.generate_medium(record, release, 1) TracklistPdf.generate_medium(release, 1)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "returns error for non-existent medium number" do test "returns error for non-existent medium number" do
record = build_record(%{title: "Test Album", artists: [%{name: "Test Artist"}]})
release = release =
build_release([ build_release([
build_medium(1, [build_track(1, "Track One", 200_000)]) build_medium(1, [build_track(1, "Track One", 200_000)])
]) ])
assert {:error, :medium_not_found} = TracklistPdf.generate_medium(record, release, 99) assert {:error, :medium_not_found} = TracklistPdf.generate_medium(release, 99)
end end
end end
@@ -132,62 +128,55 @@ defmodule MusicLibrary.Records.TracklistPdfTest do
end end
end end
describe "generate/2 with many tracks" do describe "generate/1 with many tracks" do
test "generates single-page PDF for large single-disc release" do test "generates single-page PDF for large single-disc release" do
record = build_record(%{title: "Long Album", artists: [%{name: "Prolific Artist"}]})
tracks = Enum.map(1..40, &build_track(&1, "Track #{&1}", 180_000)) tracks = Enum.map(1..40, &build_track(&1, "Track #{&1}", 180_000))
release = build_release([build_medium(1, tracks)]) release =
build_release([build_medium(1, tracks)],
title: "Long Album",
artists: [%{name: "Prolific Artist"}]
)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release) assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
test "generates single-page PDF for large multi-disc release" do test "generates single-page PDF for large multi-disc release" do
record = build_record(%{title: "Box Set", artists: [%{name: "Band"}]})
media = media =
Enum.map(1..4, fn disc -> Enum.map(1..4, fn disc ->
tracks = Enum.map(1..20, &build_track(&1, "Disc #{disc} Track #{&1}", 200_000)) tracks = Enum.map(1..20, &build_track(&1, "Disc #{disc} Track #{&1}", 200_000))
build_medium(disc, tracks) build_medium(disc, tracks)
end) end)
release = build_release(media) release =
build_release(media,
title: "Box Set",
artists: [%{name: "Band"}]
)
assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(record, release) assert {:ok, <<@pdf_magic_bytes, _::binary>> = pdf} = TracklistPdf.generate(release)
assert pdf_page_count(pdf) == 1 assert pdf_page_count(pdf) == 1
end end
end end
defp build_record(attrs) do defp build_release(media, attrs \\ []) do
artists = artists =
Enum.map(Map.get(attrs, :artists, []), fn a -> Enum.map(Keyword.get(attrs, :artists, []), fn a ->
%{ %Release.Artist{
name: a[:name] || a.name, id: "00000000-0000-0000-0000-000000000000",
sort_name: a[:sort_name] || a[:name] || a.name, name: Map.get(a, :name),
musicbrainz_id: "00000000-0000-0000-0000-000000000000", sort_name: Map.get(a, :sort_name, Map.get(a, :name, "")),
disambiguation: "", joinphrase: Map.get(a, :joinphrase)
joinphrase: a[:joinphrase] || ""
} }
end) end)
%Record{
id: Ecto.UUID.generate(),
title: attrs[:title] || "Test Album",
artists: artists,
genres: [],
type: :album,
format: :cd
}
end
defp build_release(media) do
%Release{ %Release{
id: "00000000-0000-0000-0000-000000000000", id: "00000000-0000-0000-0000-000000000000",
title: "Test Release", title: Keyword.get(attrs, :title, "Test Release"),
disambiguation: nil, disambiguation: nil,
packaging: nil, packaging: nil,
artists: [], artists: artists,
date: "2024-01-01", date: "2024-01-01",
barcode: nil, barcode: nil,
catalog_number: "", catalog_number: "",