From 0edd5f317c6057dde6e488018c9377daae4a1022 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Sun, 22 Mar 2026 08:07:06 +0000 Subject: [PATCH] Extract biography helpers from ArtistLive.Show Closes #124 --- .../live/artist_live/biography.ex | 78 ++++++++++++++++ .../live/artist_live/show.ex | 77 ++-------------- .../live/artist_live/biography_test.exs | 92 +++++++++++++++++++ 3 files changed, 176 insertions(+), 71 deletions(-) create mode 100644 lib/music_library_web/live/artist_live/biography.ex create mode 100644 test/music_library_web/live/artist_live/biography_test.exs diff --git a/lib/music_library_web/live/artist_live/biography.ex b/lib/music_library_web/live/artist_live/biography.ex new file mode 100644 index 00000000..98adeb8e --- /dev/null +++ b/lib/music_library_web/live/artist_live/biography.ex @@ -0,0 +1,78 @@ +defmodule MusicLibraryWeb.ArtistLive.Biography do + @moduledoc """ + Helper functions for building and rendering artist biographies + from Wikipedia and Last.fm data. + """ + + alias MusicLibrary.Artists.ArtistInfo + alias MusicLibraryWeb.Markdown + alias Phoenix.HTML + + @spec build(ArtistInfo.t()) :: map() | nil + def build(artist_info) do + bio_html = ArtistInfo.wikipedia_bio(artist_info) + + if bio_html do + %{ + source: "Wikipedia", + summary_html: ArtistInfo.wikipedia_summary(artist_info), + bio_html: bio_html, + url: ArtistInfo.wikipedia_url(artist_info), + description: ArtistInfo.wikipedia_description(artist_info) + } + end + end + + # Bios start with text, then a link to read more on Last.fm, followed by a license text. + # We split the bio at the read more link in order to render the license separately. + @spec render_bio(String.t()) :: Phoenix.HTML.safe() + def render_bio(bio) do + last_fm_link_regex = ~r/\.*\s*/ + + case String.split(bio, last_fm_link_regex, include_captures: true) do + [text, link, ""] -> + reformatted_bio = + Enum.join( + [ + text, + ~s(

#{link}

) + ], + "" + ) + + render_content(reformatted_bio) + + [text, link, license] -> + reformatted_bio = + Enum.join( + [ + text, + ~s(

#{link}

), + ~s(

#{license}

) + ], + "" + ) + + render_content(reformatted_bio) + + other -> + render_content(Enum.join(other, "")) + end + end + + @spec remove_read_more_link(String.t()) :: Phoenix.HTML.safe() + def remove_read_more_link(summary) do + last_fm_link_regex = ~r/\.*\s*/ + reformatted_summary = String.replace(summary, last_fm_link_regex, "") + + render_content(reformatted_summary) + end + + # sobelow_skip ["XSS.Raw"] + # Markdown.to_html/1 sanitizes HTML via MDEx (ammonia) + defp render_content(content) do + content + |> Markdown.to_html() + |> HTML.raw() + end +end diff --git a/lib/music_library_web/live/artist_live/show.ex b/lib/music_library_web/live/artist_live/show.ex index 63f60bb2..51fb3b8e 100644 --- a/lib/music_library_web/live/artist_live/show.ex +++ b/lib/music_library_web/live/artist_live/show.ex @@ -6,8 +6,8 @@ defmodule MusicLibraryWeb.ArtistLive.Show do alias MusicLibrary.{Artists, Chats, Records} alias MusicLibrary.Artists.ArtistInfo + alias MusicLibraryWeb.ArtistLive.Biography alias MusicLibraryWeb.ErrorMessages - alias MusicLibraryWeb.Markdown attr :country, :map, required: true @@ -369,7 +369,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do class="text-zinc-700 dark:text-zinc-300" >
- {remove_read_more_link(lastfm_artist_info.summary)} + {Biography.remove_read_more_link(lastfm_artist_info.summary)}
<.link class="mt-2 block text-sm font-medium text-zinc-900 dark:text-zinc-400" @@ -391,7 +391,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do placement="left" >
- {render_bio(lastfm_artist_info.bio)} + {Biography.render_bio(lastfm_artist_info.bio)}
@@ -547,7 +547,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do {:noreply, socket |> assign(:artist_info, artist_info) - |> assign(:biography, build_biography(artist_info)) + |> assign(:biography, Biography.build(artist_info)) |> put_toast(:info, gettext("Artist info refreshed successfully"))} {:error, reason} -> @@ -567,7 +567,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do {:noreply, socket |> assign(:artist_info, artist_info) - |> assign(:biography, build_biography(artist_info)) + |> assign(:biography, Biography.build(artist_info)) |> put_toast(:info, gettext("Wikipedia data refreshed successfully"))} {:error, reason} -> @@ -676,7 +676,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do |> assign(:artist, artist) |> assign(:artist_info, artist_info) |> assign(:chat_count, Chats.count_chats(:artist, musicbrainz_id)) - |> assign(:biography, build_biography(artist_info)) + |> assign(:biography, Biography.build(artist_info)) |> assign(:external_links, ArtistInfo.external_links(artist_info)) |> assign(:country, ArtistInfo.country(artist_info)) |> assign_async(:lastfm_artist_info, fn -> @@ -755,69 +755,4 @@ defmodule MusicLibraryWeb.ArtistLive.Show do wishlist: Enum.sort_by(wishlist, fn r -> r.release_date end, :desc) } end - - defp build_biography(artist_info) do - bio_html = ArtistInfo.wikipedia_bio(artist_info) - - if bio_html do - %{ - source: "Wikipedia", - summary_html: ArtistInfo.wikipedia_summary(artist_info), - bio_html: bio_html, - url: ArtistInfo.wikipedia_url(artist_info), - description: ArtistInfo.wikipedia_description(artist_info) - } - end - end - - # Bios start with text, then a link to read more on Last.fm, followed by a license text. - # We split the bio at the read more link in order to render the license separately. - defp render_bio(bio) do - last_fm_link_regex = ~r/\.*\s*/ - - case String.split(bio, last_fm_link_regex, include_captures: true) do - [text, link, ""] -> - reformatted_bio = - Enum.join( - [ - text, - ~s(

#{link}

) - ], - "" - ) - - render_content(reformatted_bio) - - [text, link, license] -> - reformatted_bio = - Enum.join( - [ - text, - ~s(

#{link}

), - ~s(

#{license}

) - ], - "" - ) - - render_content(reformatted_bio) - - other -> - render_content(other) - end - end - - defp remove_read_more_link(summary) do - last_fm_link_regex = ~r/\.*\s*/ - reformatted_summary = String.replace(summary, last_fm_link_regex, "") - - render_content(reformatted_summary) - end - - # sobelow_skip ["XSS.Raw"] - # Markdown.to_html/1 sanitizes HTML via MDEx (ammonia) - defp render_content(content) do - content - |> Markdown.to_html() - |> raw() - end end diff --git a/test/music_library_web/live/artist_live/biography_test.exs b/test/music_library_web/live/artist_live/biography_test.exs new file mode 100644 index 00000000..0ea1fc35 --- /dev/null +++ b/test/music_library_web/live/artist_live/biography_test.exs @@ -0,0 +1,92 @@ +defmodule MusicLibraryWeb.ArtistLive.BiographyTest do + use MusicLibrary.DataCase, async: true + + alias MusicLibraryWeb.ArtistLive.Biography + + import MusicLibrary.ArtistInfoFixtures + + describe "build/1" do + test "returns biography map when Wikipedia data is present" do + artist_info = + artist_info_fixture(%{ + wikipedia_data: %{ + "intro_html" => "

A musician from England.

", + "extract" => "A musician from England.", + "content_urls" => %{"desktop" => %{"page" => "https://en.wikipedia.org/wiki/Test"}}, + "description" => "English musician" + } + }) + + result = Biography.build(artist_info) + + assert result.source == "Wikipedia" + assert result.bio_html == "

A musician from England.

" + assert result.summary_html == "A musician from England." + assert result.url == "https://en.wikipedia.org/wiki/Test" + assert result.description == "English musician" + end + + test "returns nil when no Wikipedia data is present" do + artist_info = artist_info_fixture(%{wikipedia_data: nil}) + + assert Biography.build(artist_info) == nil + end + + test "returns nil when Wikipedia data has no bio" do + artist_info = artist_info_fixture(%{wikipedia_data: %{}}) + + assert Biography.build(artist_info) == nil + end + end + + describe "render_bio/1" do + test "renders bio with Last.fm link and no license" do + bio = + ~s(Some artist biography text. Read more on Last.fm) + + result = Phoenix.HTML.safe_to_string(Biography.render_bio(bio)) + + assert result =~ "Some artist biography text." + assert result =~ "Read more on Last.fm" + end + + test "renders bio with Last.fm link and license text" do + bio = + ~s(Some artist biography text. Read more on Last.fm. User-contributed text is available under the Creative Commons License.) + + result = Phoenix.HTML.safe_to_string(Biography.render_bio(bio)) + + assert result =~ "Some artist biography text." + assert result =~ "Read more on Last.fm" + assert result =~ "Creative Commons License" + end + + test "renders plain text bio without Last.fm link" do + bio = "Just a plain biography." + + result = Biography.render_bio(bio) + + assert Phoenix.HTML.safe_to_string(result) =~ "Just a plain biography." + end + end + + describe "remove_read_more_link/1" do + test "strips Last.fm read more link from summary" do + summary = + ~s(Some summary text. Read more on Last.fm.) + + result = Biography.remove_read_more_link(summary) + + assert Phoenix.HTML.safe_to_string(result) =~ "Some summary text." + refute Phoenix.HTML.safe_to_string(result) =~ "Read more on Last.fm" + end + + test "returns content as-is when no Last.fm link present" do + summary = "A summary without links." + + result = Biography.remove_read_more_link(summary) + + assert Phoenix.HTML.safe_to_string(result) =~ "A summary without links." + end + end +end