Add function to download a record's tracklist for printing
This commit is contained in:
@@ -89,6 +89,19 @@ window.addEventListener("music_library:confetti", (_event) => {
|
|||||||
spread: 200,
|
spread: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
window.addEventListener("phx:music_library:download", (event) => {
|
||||||
|
const { data, filename, content_type } = event.detail;
|
||||||
|
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
||||||
|
const blob = new Blob([bytes], { type: content_type });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
// connect if there are any LiveViews on the page
|
// connect if there are any LiveViews on the page
|
||||||
liveSocket.connect();
|
liveSocket.connect();
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ Last.fm schemas (separate, not Ecto-persisted to main DB):
|
|||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `Records.SearchParser` | Parses search syntax: `artist:X`, `album:X`, `genre:"Y"`, `format:cd`, `type:album`, `purchase_year:2024`, free text |
|
| `Records.SearchParser` | Parses search syntax: `artist:X`, `album:X`, `genre:"Y"`, `format:cd`, `type:album`, `purchase_year:2024`, free text |
|
||||||
| `Records.Similarity` | Embedding generation (OpenAI, enriched with Last.fm tags), cosine-distance search (sqlite-vec) |
|
| `Records.Similarity` | Embedding generation (OpenAI, enriched with Last.fm tags), cosine-distance search (sqlite-vec) |
|
||||||
|
| `Records.TracklistPdf` | Generates 120mm×120mm PDF tracklist from record + release data (Typst) |
|
||||||
| `Batch` | Generic batch runner: stream + transaction + error accumulation |
|
| `Batch` | Generic batch runner: stream + transaction + error accumulation |
|
||||||
| `Records.Batch` | Batch operations: refresh all MusicBrainz data, generate all embeddings (uses `Batch`) |
|
| `Records.Batch` | Batch operations: refresh all MusicBrainz data, generate all embeddings (uses `Batch`) |
|
||||||
| `Artists.Batch` | Batch refresh: MusicBrainz, Discogs, Wikipedia, Last.fm for all artists (uses `Batch`) |
|
| `Artists.Batch` | Batch refresh: MusicBrainz, Discogs, Wikipedia, Last.fm for all artists (uses `Batch`) |
|
||||||
@@ -324,6 +325,7 @@ All events are namespaced with `music_library:` prefix.
|
|||||||
| `music_library:clipcopy` | Copy text to clipboard |
|
| `music_library:clipcopy` | Copy text to clipboard |
|
||||||
| `music_library:scroll_top` | Scroll window to top |
|
| `music_library:scroll_top` | Scroll window to top |
|
||||||
| `music_library:confetti` | Trigger canvas-confetti animation |
|
| `music_library:confetti` | Trigger canvas-confetti animation |
|
||||||
|
| `music_library:download` | Decode base64 blob and trigger browser file download (dispatched via `push_event`, prefixed `phx:` on client) |
|
||||||
|
|
||||||
### NPM Dependencies
|
### NPM Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
defmodule MusicLibrary.Records.TracklistPdf do
|
||||||
|
alias MusicBrainz.Release
|
||||||
|
alias MusicLibraryWeb.Duration
|
||||||
|
alias Typst.Format
|
||||||
|
|
||||||
|
@spec generate(MusicLibrary.Records.Record.t(), Release.t()) ::
|
||||||
|
{:ok, binary()} | {:error, term()}
|
||||||
|
def generate(record, release) do
|
||||||
|
markup = build_markup(record, release)
|
||||||
|
Typst.render_to_pdf(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_markup(record, release) do
|
||||||
|
"""
|
||||||
|
#set page(width: 120mm, height: 120mm, margin: 5mm)
|
||||||
|
#set text(font: "Liberation Sans", size: 8pt)
|
||||||
|
|
||||||
|
#align(center)[
|
||||||
|
#text(size: 10pt, weight: "bold")[#{Format.escape(artist_names(record))}]
|
||||||
|
#linebreak()
|
||||||
|
#text(size: 9pt, style: "italic")[#{Format.escape(record.title)}]
|
||||||
|
]
|
||||||
|
|
||||||
|
#v(3mm)
|
||||||
|
|
||||||
|
#{build_media(release)}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_media(release) do
|
||||||
|
media_count = Release.media_count(release)
|
||||||
|
|
||||||
|
release.media
|
||||||
|
|> Enum.map_join("\n", fn medium ->
|
||||||
|
build_medium(medium, media_count)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_medium(medium, media_count) do
|
||||||
|
header =
|
||||||
|
if media_count > 1 do
|
||||||
|
label = medium_label(medium)
|
||||||
|
|
||||||
|
"""
|
||||||
|
#v(2mm)
|
||||||
|
#text(size: 8pt, weight: "bold")[#{Format.escape(label)}]
|
||||||
|
#v(1mm)
|
||||||
|
"""
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
tracks =
|
||||||
|
medium.tracks
|
||||||
|
|> Enum.map_join("\n", &build_track/1)
|
||||||
|
|
||||||
|
header <> tracks
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_track(track) do
|
||||||
|
number = Format.escape(track.number || to_string(track.position))
|
||||||
|
title = Format.escape(track.title)
|
||||||
|
|
||||||
|
duration =
|
||||||
|
if track.length do
|
||||||
|
Format.escape(Duration.format_duration(track.length))
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
#grid(
|
||||||
|
columns: (12pt, 1fr, auto),
|
||||||
|
gutter: 4pt,
|
||||||
|
align(right)[#{number}],
|
||||||
|
[#{title}],
|
||||||
|
align(right)[#{duration}]
|
||||||
|
)\
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp artist_names(record) do
|
||||||
|
Enum.map_join(record.artists, fn artist ->
|
||||||
|
artist.name <> (artist.joinphrase || "")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp medium_label(medium) do
|
||||||
|
title =
|
||||||
|
if medium.title && medium.title != "" do
|
||||||
|
medium.title
|
||||||
|
else
|
||||||
|
"Disc #{medium.number}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if medium.format do
|
||||||
|
"#{title} (#{medium.format})"
|
||||||
|
else
|
||||||
|
title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias MusicBrainz.Release
|
alias MusicBrainz.Release
|
||||||
|
alias MusicLibrary.Records.TracklistPdf
|
||||||
alias MusicLibrary.ScrobbleActivity
|
alias MusicLibrary.ScrobbleActivity
|
||||||
alias MusicLibraryWeb.Duration
|
alias MusicLibraryWeb.Duration
|
||||||
alias MusicLibraryWeb.ErrorMessages
|
alias MusicLibraryWeb.ErrorMessages
|
||||||
@@ -69,23 +70,34 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
/>
|
/>
|
||||||
{gettext("Tracks")}
|
{gettext("Tracks")}
|
||||||
</label>
|
</label>
|
||||||
<.button
|
<div class="flex items-center gap-2">
|
||||||
:if={@can_scrobble? && @release_with_tracks.ok?}
|
<.button
|
||||||
size="sm"
|
:if={@release_with_tracks.ok?}
|
||||||
disabled={@already_scrobbled}
|
variant="ghost"
|
||||||
phx-click={
|
size="sm"
|
||||||
if MapSet.size(@selected_tracks) > 0,
|
phx-click="print_tracklist"
|
||||||
do: "scrobble_selected_tracks",
|
phx-target={@myself}
|
||||||
else: "scrobble_release"
|
>
|
||||||
}
|
<.icon name="hero-printer" class="h-4 w-4" />
|
||||||
phx-target={@myself}
|
</.button>
|
||||||
phx-disable-with={gettext("Scrobbling...")}
|
<.button
|
||||||
>
|
:if={@can_scrobble? && @release_with_tracks.ok?}
|
||||||
{scrobble_button_label(@selected_tracks)}
|
size="sm"
|
||||||
</.button>
|
disabled={@already_scrobbled}
|
||||||
<.button :if={!@can_scrobble?} size="sm" href={LastFm.auth_url()}>
|
phx-click={
|
||||||
{gettext("Connect your Last.fm account")}
|
if MapSet.size(@selected_tracks) > 0,
|
||||||
</.button>
|
do: "scrobble_selected_tracks",
|
||||||
|
else: "scrobble_release"
|
||||||
|
}
|
||||||
|
phx-target={@myself}
|
||||||
|
phx-disable-with={gettext("Scrobbling...")}
|
||||||
|
>
|
||||||
|
{scrobble_button_label(@selected_tracks)}
|
||||||
|
</.button>
|
||||||
|
<.button :if={!@can_scrobble?} size="sm" href={LastFm.auth_url()}>
|
||||||
|
{gettext("Connect your Last.fm account")}
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :if={@release_with_tracks} class="space-y-4 mt-4">
|
<div :if={@release_with_tracks} class="space-y-4 mt-4">
|
||||||
@@ -387,6 +399,38 @@ defmodule MusicLibraryWeb.Components.Release do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("print_tracklist", _params, socket) when release_loaded?(socket.assigns) do
|
||||||
|
release = socket.assigns.release_with_tracks.result
|
||||||
|
record = socket.assigns.record
|
||||||
|
|
||||||
|
case TracklistPdf.generate(record, release) do
|
||||||
|
{:ok, pdf_binary} ->
|
||||||
|
filename = "#{record.title} - 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 tracklist PDF: #{inspect(reason)}")
|
||||||
|
|
||||||
|
put_toast!(
|
||||||
|
:error,
|
||||||
|
gettext("Error generating tracklist PDF") <>
|
||||||
|
": " <> ErrorMessages.friendly_message(reason)
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("print_tracklist", _params, socket) do
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("load_release_tracks", _params, socket) do
|
def handle_event("load_release_tracks", _params, socket) do
|
||||||
selected_release_id = socket.assigns.record.selected_release_id
|
selected_release_id = socket.assigns.record.selected_release_id
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ defmodule MusicLibrary.MixProject do
|
|||||||
# Notes
|
# Notes
|
||||||
{:mdex, "~> 0.11.6"},
|
{:mdex, "~> 0.11.6"},
|
||||||
|
|
||||||
|
# PDF generation
|
||||||
|
{:typst, "~> 0.3.1"},
|
||||||
|
|
||||||
# Syntax highlighting
|
# Syntax highlighting
|
||||||
{:lumis, "~> 0.1"},
|
{:lumis, "~> 0.1"},
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||||
"tidewave": {:hex, :tidewave, "0.5.5", "a125dfc87f99daf0e2280b3a9719b874c616ead5926cdf9cdfe4fcc19a020eff", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "825ebb4fa20de005785efa21e5a88c04d81c3f57552638d12ff3def2f203dbf7"},
|
"tidewave": {:hex, :tidewave, "0.5.5", "a125dfc87f99daf0e2280b3a9719b874c616ead5926cdf9cdfe4fcc19a020eff", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "825ebb4fa20de005785efa21e5a88c04d81c3f57552638d12ff3def2f203dbf7"},
|
||||||
"time_zone_info": {:hex, :time_zone_info, "0.7.12", "17d3e841e0421258c42c5cbb0b9a008935a7cf5db184ee8307510f133f0bed09", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1b131992044bf1306bf6ce5f7246c29ed3769548c141249cc28991fa8ec13ed2"},
|
"time_zone_info": {:hex, :time_zone_info, "0.7.12", "17d3e841e0421258c42c5cbb0b9a008935a7cf5db184ee8307510f133f0bed09", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1b131992044bf1306bf6ce5f7246c29ed3769548c141249cc28991fa8ec13ed2"},
|
||||||
|
"typst": {:hex, :typst, "0.3.1", "fc0a455feaece0e1e352a92082c22a6c8ade89f0e3661c5375f66a1818853270", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "545cae10fd2727316a40014832e4b1aac076f35e29e78c2206397f1a11cba790"},
|
||||||
"usage_rules": {:hex, :usage_rules, "1.2.4", "c718c429dd7de89bf57cd3e75fe218108fcdd342e34aa6ccac7123ae676c532d", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "2b9a913340b2a74e327b64eedc1cdd58d71b64c8fce09b8d18acb2db3884a548"},
|
"usage_rules": {:hex, :usage_rules, "1.2.4", "c718c429dd7de89bf57cd3e75fe218108fcdd342e34aa6ccac7123ae676c532d", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "2b9a913340b2a74e327b64eedc1cdd58d71b64c8fce09b8d18acb2db3884a548"},
|
||||||
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
|
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
|
|||||||
@@ -2200,3 +2200,8 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "validation failed"
|
msgid "validation failed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Error generating tracklist PDF"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -2200,3 +2200,8 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "validation failed"
|
msgid "validation failed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/release.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Error generating tracklist PDF"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
defmodule MusicLibrary.Records.TracklistPdfTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias MusicBrainz.Release
|
||||||
|
alias MusicLibrary.Records.{Record, TracklistPdf}
|
||||||
|
|
||||||
|
@pdf_magic_bytes <<37, 80, 68, 70>>
|
||||||
|
|
||||||
|
describe "generate/2" do
|
||||||
|
test "generates PDF for single-disc release" do
|
||||||
|
record = build_record(%{title: "OK Computer", artists: [%{name: "Radiohead"}]})
|
||||||
|
|
||||||
|
release =
|
||||||
|
build_release([
|
||||||
|
build_medium(1, [
|
||||||
|
build_track(1, "Airbag", 284_533),
|
||||||
|
build_track(2, "Paranoid Android", 383_000)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
assert {:ok, <<@pdf_magic_bytes, _::binary>>} = TracklistPdf.generate(record, release)
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
release = Release.from_api_response(api_response)
|
||||||
|
|
||||||
|
assert {:ok, <<@pdf_magic_bytes, _::binary>>} = TracklistPdf.generate(record, release)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders artist joinphrase correctly" do
|
||||||
|
record =
|
||||||
|
build_record(%{
|
||||||
|
title: "Collab Album",
|
||||||
|
artists: [
|
||||||
|
%{name: "Artist A", joinphrase: " & "},
|
||||||
|
%{name: "Artist B"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
release =
|
||||||
|
build_release([
|
||||||
|
build_medium(1, [build_track(1, "Track One", 200_000)])
|
||||||
|
])
|
||||||
|
|
||||||
|
assert {:ok, <<@pdf_magic_bytes, _::binary>>} = TracklistPdf.generate(record, release)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles track without duration" do
|
||||||
|
record = build_record(%{title: "Test Album", artists: [%{name: "Test Artist"}]})
|
||||||
|
|
||||||
|
release =
|
||||||
|
build_release([
|
||||||
|
build_medium(1, [
|
||||||
|
build_track(1, "Has Duration", 180_000),
|
||||||
|
build_track(2, "No Duration", nil)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
assert {:ok, <<@pdf_magic_bytes, _::binary>>} = TracklistPdf.generate(record, release)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles special characters in title" do
|
||||||
|
record =
|
||||||
|
build_record(%{
|
||||||
|
title: "Album *with* #special @chars",
|
||||||
|
artists: [%{name: "Artist #1"}]
|
||||||
|
})
|
||||||
|
|
||||||
|
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>>} = TracklistPdf.generate(record, release)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_record(attrs) do
|
||||||
|
artists =
|
||||||
|
Enum.map(Map.get(attrs, :artists, []), fn a ->
|
||||||
|
%{
|
||||||
|
name: a[:name] || a.name,
|
||||||
|
sort_name: a[:sort_name] || a[:name] || a.name,
|
||||||
|
musicbrainz_id: "00000000-0000-0000-0000-000000000000",
|
||||||
|
disambiguation: "",
|
||||||
|
joinphrase: a[:joinphrase] || ""
|
||||||
|
}
|
||||||
|
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{
|
||||||
|
id: "00000000-0000-0000-0000-000000000000",
|
||||||
|
title: "Test Release",
|
||||||
|
disambiguation: nil,
|
||||||
|
packaging: nil,
|
||||||
|
artists: [],
|
||||||
|
date: "2024-01-01",
|
||||||
|
barcode: nil,
|
||||||
|
catalog_number: "",
|
||||||
|
country: nil,
|
||||||
|
media: media
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_medium(number, tracks) do
|
||||||
|
%Release.Medium{
|
||||||
|
title: nil,
|
||||||
|
format: "CD",
|
||||||
|
number: number,
|
||||||
|
track_count: length(tracks),
|
||||||
|
tracks: tracks
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_track(position, title, length) do
|
||||||
|
%Release.Track{
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
title: title,
|
||||||
|
artists: [],
|
||||||
|
length: length,
|
||||||
|
number: to_string(position),
|
||||||
|
position: position
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user