Refactor TracklistPdf to take release alone
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user