Support getting artist bios from Wikipedia (experimental)
This commit is contained in:
@@ -35,7 +35,9 @@
|
||||
"Bash(mix compile:*)",
|
||||
"mcp__tidewave__get_docs",
|
||||
"mcp__tidewave__search_package_docs",
|
||||
"Bash(wc:*)"
|
||||
"Bash(wc:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(MIX_ENV=test mix ecto.migrate:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": false
|
||||
|
||||
+3
-1
@@ -47,6 +47,8 @@ config :music_library, MusicBrainz, user_agent: user_agent
|
||||
|
||||
config :music_library, Discogs, personal_access_token: "change me", user_agent: user_agent
|
||||
|
||||
config :music_library, Wikipedia, user_agent: user_agent
|
||||
|
||||
# Configure esbuild (the version is required)
|
||||
config :esbuild,
|
||||
version: "0.27.3",
|
||||
@@ -78,7 +80,7 @@ config :phoenix, :json_library, JSON
|
||||
|
||||
config :music_library, Oban,
|
||||
engine: Oban.Engines.Lite,
|
||||
queues: [default: 10, heavy_writes: 1, music_brainz: 1, discogs: 1],
|
||||
queues: [default: 10, heavy_writes: 1, music_brainz: 1, discogs: 1, wikipedia: 1],
|
||||
repo: MusicLibrary.BackgroundRepo,
|
||||
plugins: [
|
||||
{Oban.Plugins.Cron,
|
||||
|
||||
@@ -57,6 +57,12 @@ config :music_library, Discogs,
|
||||
max_retries: 0
|
||||
]
|
||||
|
||||
config :music_library, Wikipedia,
|
||||
req_options: [
|
||||
plug: {Req.Test, Wikipedia.API},
|
||||
max_retries: 0
|
||||
]
|
||||
|
||||
config :phoenix_test, :endpoint, MusicLibraryWeb.Endpoint
|
||||
|
||||
config :music_library, Oban, testing: :manual
|
||||
|
||||
@@ -32,6 +32,14 @@ defmodule MusicBrainz.Artist do
|
||||
end
|
||||
end
|
||||
|
||||
def get_wikidata_id(r) do
|
||||
Enum.find_value(r.relations, fn relation ->
|
||||
if relation.type == "wikidata" do
|
||||
parse_wikidata_id(relation.url["resource"])
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def url(id) do
|
||||
"https://musicbrainz.org/artist/#{id}"
|
||||
end
|
||||
@@ -47,4 +55,7 @@ defmodule MusicBrainz.Artist do
|
||||
|
||||
defp parse_discogs_id("https://www.discogs.com/artist/" <> id), do: String.to_integer(id)
|
||||
defp parse_discogs_id(_other), do: nil
|
||||
|
||||
defp parse_wikidata_id("https://www.wikidata.org/wiki/" <> id), do: id
|
||||
defp parse_wikidata_id(_other), do: nil
|
||||
end
|
||||
|
||||
@@ -134,6 +134,32 @@ defmodule MusicLibrary.Artists do
|
||||
enqueue_worker(Worker.ArtistRefreshDiscogsData, %{"id" => artist_info.id})
|
||||
end
|
||||
|
||||
def fetch_wikipedia_data(artist_id) do
|
||||
artist_info = get_artist_info!(artist_id)
|
||||
|
||||
if wikidata_id = ArtistInfo.wikidata_id(artist_info) do
|
||||
case Wikipedia.get_artist_summary(wikidata_id) do
|
||||
{:ok, summary} ->
|
||||
artist_info
|
||||
|> ArtistInfo.changeset(%{wikipedia_data: summary})
|
||||
|> Repo.update()
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
else
|
||||
{:ok, artist_info}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_wikipedia_data(artist_id) do
|
||||
fetch_wikipedia_data(artist_id)
|
||||
end
|
||||
|
||||
def refresh_wikipedia_data_async(artist_info) do
|
||||
enqueue_worker(Worker.ArtistRefreshWikipediaData, %{"id" => artist_info.id})
|
||||
end
|
||||
|
||||
def create_artist_info(attrs) do
|
||||
%ArtistInfo{}
|
||||
|> ArtistInfo.changeset(attrs)
|
||||
|
||||
@@ -10,6 +10,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
|
||||
schema "artist_infos" do
|
||||
field :musicbrainz_data, :map, default: %{}
|
||||
field :discogs_data, :map, default: %{}
|
||||
field :wikipedia_data, :map, default: %{}
|
||||
field :image_data_hash, :string
|
||||
|
||||
has_one :note, Note, foreign_key: :musicbrainz_id
|
||||
@@ -23,6 +24,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
|
||||
:id,
|
||||
:musicbrainz_data,
|
||||
:discogs_data,
|
||||
:wikipedia_data,
|
||||
:image_data_hash
|
||||
])
|
||||
|> validate_required([:musicbrainz_data])
|
||||
@@ -82,4 +84,33 @@ defmodule MusicLibrary.Artists.ArtistInfo do
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def wikidata_id(artist_info) do
|
||||
relations = get_in(artist_info.musicbrainz_data, ["relations"]) || []
|
||||
|
||||
Enum.find_value(relations, fn
|
||||
%{"type" => "wikidata", "url" => %{"resource" => "https://www.wikidata.org/wiki/" <> id}} ->
|
||||
id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
def wikipedia_bio(artist_info) do
|
||||
get_in(artist_info.wikipedia_data, ["intro_html"]) ||
|
||||
get_in(artist_info.wikipedia_data, ["extract_html"])
|
||||
end
|
||||
|
||||
def wikipedia_summary(artist_info) do
|
||||
get_in(artist_info.wikipedia_data, ["extract"])
|
||||
end
|
||||
|
||||
def wikipedia_url(artist_info) do
|
||||
get_in(artist_info.wikipedia_data, ["content_urls", "desktop", "page"])
|
||||
end
|
||||
|
||||
def wikipedia_description(artist_info) do
|
||||
get_in(artist_info.wikipedia_data, ["description"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
defmodule MusicLibrary.Worker.ArtistRefreshWikipediaData do
|
||||
use Oban.Worker, queue: :wikipedia, max_attempts: 3
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => artist_info_id}}) do
|
||||
result = MusicLibrary.Artists.refresh_wikipedia_data(artist_info_id)
|
||||
|
||||
Process.sleep(1_000)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ defmodule MusicLibrary.Worker.FetchArtistInfo do
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"id" => artist_id}}) do
|
||||
with {:ok, _artist_info} <- MusicLibrary.Artists.fetch_artist_info(artist_id),
|
||||
{:ok, _artist_info} <- MusicLibrary.Artists.fetch_wikipedia_data(artist_id),
|
||||
{:ok, _artist_info} <- MusicLibrary.Artists.fetch_image(artist_id) do
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -101,6 +101,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:artist_info, artist_info)
|
||||
|> assign(:biography, build_biography(artist_info))
|
||||
|> put_toast(:info, gettext("Artist info refreshed successfully"))}
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -113,6 +114,25 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("refresh_wikipedia_data", %{"id" => id}, socket) do
|
||||
case Artists.refresh_wikipedia_data(id) do
|
||||
{:ok, artist_info} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:artist_info, artist_info)
|
||||
|> assign(:biography, build_biography(artist_info))
|
||||
|> put_toast(:info, gettext("Wikipedia data refreshed successfully"))}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_toast(
|
||||
:error,
|
||||
gettext("Error refreshing Wikipedia data") <> "," <> inspect(reason)
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("refresh_artist_image", %{"id" => id}, socket) do
|
||||
case Artists.fetch_image(id) do
|
||||
{:ok, artist_info} ->
|
||||
@@ -171,6 +191,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
|
||||
|> assign(:current_section, :artists)
|
||||
|> assign(:artist, artist)
|
||||
|> assign(:artist_info, artist_info)
|
||||
|> assign(:biography, build_biography(artist_info))
|
||||
|> assign(:external_links, ArtistInfo.external_links(artist_info))
|
||||
|> assign(:country, ArtistInfo.country(artist_info))
|
||||
|> assign_async(:lastfm_artist_info, fn ->
|
||||
@@ -250,6 +271,20 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
|
||||
}
|
||||
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
|
||||
|
||||
@@ -73,6 +73,20 @@
|
||||
/>
|
||||
{gettext("Refresh info")}
|
||||
</.dropdown_link>
|
||||
<.dropdown_link
|
||||
id={"actions-#{@artist.musicbrainz_id}-refresh-wikipedia"}
|
||||
phx-click={
|
||||
JS.push("refresh_wikipedia_data", value: %{id: @artist.musicbrainz_id})
|
||||
}
|
||||
>
|
||||
<.icon
|
||||
name="hero-arrow-path"
|
||||
class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Refresh Wikipedia")}
|
||||
</.dropdown_link>
|
||||
</.focus_wrap>
|
||||
</.dropdown>
|
||||
</.button_group>
|
||||
@@ -141,34 +155,19 @@
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
|
||||
<:loading>
|
||||
<div class="mt-4 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
{gettext("Loading biography")}
|
||||
</div>
|
||||
</:loading>
|
||||
<:failed :let={_failure}>
|
||||
<div class="mt-4 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="-mt-1 mr-1 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Error loading biography")}
|
||||
</div>
|
||||
</:failed>
|
||||
<dt
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
class="mt-4 text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"
|
||||
>
|
||||
<%= if @biography do %>
|
||||
<dt class="mt-4 text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
|
||||
{gettext("Biography")}
|
||||
<.badge variant="soft" class="ml-1">{@biography.source}</.badge>
|
||||
</dt>
|
||||
<dd
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
class="text-zinc-700 dark:text-zinc-300"
|
||||
>
|
||||
{remove_read_more_link(lastfm_artist_info.summary)}
|
||||
<dd class="text-zinc-700 dark:text-zinc-300">
|
||||
<p
|
||||
:if={@biography.description}
|
||||
class="mt-2 text-sm italic text-zinc-500 dark:text-zinc-400"
|
||||
>
|
||||
{@biography.description}
|
||||
</p>
|
||||
<p class="mt-2 text-sm/7">{@biography.summary_html}</p>
|
||||
<.link
|
||||
class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
|
||||
phx-click={Fluxon.open_dialog("bio")}
|
||||
@@ -183,14 +182,80 @@
|
||||
</.link>
|
||||
</dd>
|
||||
<.sheet
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
id="bio"
|
||||
class="max-w-2xl text-zinc-700 dark:text-zinc-300"
|
||||
placement="left"
|
||||
>
|
||||
{render_bio(lastfm_artist_info.bio)}
|
||||
<div class="prose prose-sm dark:prose-invert">
|
||||
{Phoenix.HTML.raw(@biography.bio_html)}
|
||||
</div>
|
||||
<a
|
||||
:if={@biography.url}
|
||||
href={@biography.url}
|
||||
target="_blank"
|
||||
class="mt-4 block text-sm font-medium text-zinc-900 dark:text-zinc-400 hover:text-zinc-500"
|
||||
>
|
||||
{gettext("Read full article on Wikipedia")}
|
||||
<.icon
|
||||
name="hero-arrow-top-right-on-square"
|
||||
class="-mt-1 ml-1 h-4 w-4"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
</a>
|
||||
</.sheet>
|
||||
</.async_result>
|
||||
<% else %>
|
||||
<.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
|
||||
<:loading>
|
||||
<div class="mt-4 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
{gettext("Loading biography")}
|
||||
</div>
|
||||
</:loading>
|
||||
<:failed :let={_failure}>
|
||||
<div class="mt-4 text-sm leading-5 text-zinc-500 dark:text-zinc-400">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="-mt-1 mr-1 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Error loading biography")}
|
||||
</div>
|
||||
</:failed>
|
||||
<dt
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
class="mt-4 text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400"
|
||||
>
|
||||
{gettext("Biography")}
|
||||
</dt>
|
||||
<dd
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
class="text-zinc-700 dark:text-zinc-300"
|
||||
>
|
||||
{remove_read_more_link(lastfm_artist_info.summary)}
|
||||
<.link
|
||||
class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
|
||||
phx-click={Fluxon.open_dialog("lastfm-bio")}
|
||||
>
|
||||
<.icon
|
||||
name="hero-arrow-right-end-on-rectangle"
|
||||
class="-mt-1 mr-1 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
data-slot="icon"
|
||||
/>
|
||||
{gettext("Read more")}
|
||||
</.link>
|
||||
</dd>
|
||||
<.sheet
|
||||
:if={lastfm_artist_info.bio not in [nil, ""]}
|
||||
id="lastfm-bio"
|
||||
class="max-w-2xl text-zinc-700 dark:text-zinc-300"
|
||||
placement="left"
|
||||
>
|
||||
{render_bio(lastfm_artist_info.bio)}
|
||||
</.sheet>
|
||||
</.async_result>
|
||||
<% end %>
|
||||
<.external_links external_links={@external_links} />
|
||||
</div>
|
||||
<div class="md:col-span-7 md:order-1">
|
||||
@@ -251,6 +316,12 @@
|
||||
title={gettext("Discogs data")}
|
||||
data={@artist_info.discogs_data}
|
||||
/>
|
||||
|
||||
<.json_viewer
|
||||
:if={@artist_info.wikipedia_data != %{}}
|
||||
title={gettext("Wikipedia data")}
|
||||
data={@artist_info.wikipedia_data}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.live_component
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
defmodule Wikipedia do
|
||||
alias Wikipedia.API
|
||||
|
||||
def get_artist_summary(wikidata_id) do
|
||||
config = wikipedia_config()
|
||||
|
||||
with {:ok, title} when not is_nil(title) <- API.get_wikipedia_title(wikidata_id, config),
|
||||
{:ok, summary} <- API.get_article_summary(title, config),
|
||||
{:ok, intro_html} <- API.get_article_extract(title, config) do
|
||||
{:ok, Map.put(summary, "intro_html", intro_html)}
|
||||
else
|
||||
{:ok, nil} -> {:error, :no_english_wikipedia}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp wikipedia_config, do: Wikipedia.Config.resolve(:music_library)
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
defmodule Wikipedia.API do
|
||||
@moduledoc """
|
||||
Interface to the Wikidata and Wikipedia APIs.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
def get_wikipedia_title(wikidata_id, config) do
|
||||
request =
|
||||
Req.new(
|
||||
base_url: "https://www.wikidata.org",
|
||||
max_retries: 1,
|
||||
user_agent: config.user_agent
|
||||
)
|
||||
|> Req.Request.merge_options(config.req_options)
|
||||
|> Req.Request.append_request_steps(log_attempt: &log_attempt/1)
|
||||
|> Req.Request.append_response_steps(log_error: &log_error/1)
|
||||
|> Req.merge(
|
||||
url: "/w/api.php",
|
||||
params: [
|
||||
action: "wbgetentities",
|
||||
ids: wikidata_id,
|
||||
props: "sitelinks",
|
||||
sitefilter: "enwiki",
|
||||
format: "json"
|
||||
]
|
||||
)
|
||||
|
||||
case Req.get(request) do
|
||||
{:ok, response} when response.status == 200 ->
|
||||
title =
|
||||
get_in(response.body, ["entities", wikidata_id, "sitelinks", "enwiki", "title"])
|
||||
|
||||
{:ok, title}
|
||||
|
||||
{:ok, response} ->
|
||||
{:error, response.body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def get_article_summary(title, config) do
|
||||
request =
|
||||
Req.new(
|
||||
base_url: "https://en.wikipedia.org",
|
||||
max_retries: 1,
|
||||
user_agent: config.user_agent
|
||||
)
|
||||
|> Req.Request.merge_options(config.req_options)
|
||||
|> Req.Request.append_request_steps(log_attempt: &log_attempt/1)
|
||||
|> Req.Request.append_response_steps(log_error: &log_error/1)
|
||||
|> Req.merge(url: "/api/rest_v1/page/summary/#{URI.encode(title)}")
|
||||
|
||||
case Req.get(request) do
|
||||
{:ok, response} when response.status == 200 ->
|
||||
{:ok, response.body}
|
||||
|
||||
{:ok, response} ->
|
||||
{:error, response.body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def get_article_extract(title, config) do
|
||||
request =
|
||||
Req.new(
|
||||
base_url: "https://en.wikipedia.org",
|
||||
max_retries: 1,
|
||||
user_agent: config.user_agent
|
||||
)
|
||||
|> Req.Request.merge_options(config.req_options)
|
||||
|> Req.Request.append_request_steps(log_attempt: &log_attempt/1)
|
||||
|> Req.Request.append_response_steps(log_error: &log_error/1)
|
||||
|> Req.merge(
|
||||
url: "/w/api.php",
|
||||
params: [
|
||||
action: "query",
|
||||
titles: title,
|
||||
prop: "extracts",
|
||||
exintro: "1",
|
||||
format: "json"
|
||||
]
|
||||
)
|
||||
|
||||
case Req.get(request) do
|
||||
{:ok, response} when response.status == 200 ->
|
||||
extract =
|
||||
response.body
|
||||
|> get_in(["query", "pages"])
|
||||
|> Map.values()
|
||||
|> List.first()
|
||||
|> Map.get("extract")
|
||||
|
||||
{:ok, extract}
|
||||
|
||||
{:ok, response} ->
|
||||
{:error, response.body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp log_attempt(request) do
|
||||
url = URI.to_string(request.url)
|
||||
Logger.debug("Fetching data from #{url}")
|
||||
request
|
||||
end
|
||||
|
||||
defp log_error({request, response}) do
|
||||
if response.status in 400..499 or response.status in 500..599 do
|
||||
Logger.error(fn ->
|
||||
url = URI.to_string(request.url)
|
||||
"Failed to fetch data from #{url}, reason: #{inspect(response.body)}"
|
||||
end)
|
||||
end
|
||||
|
||||
{request, response}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
defmodule Wikipedia.Config do
|
||||
@type t :: %__MODULE__{
|
||||
user_agent: String.t(),
|
||||
req_options: Keyword.t()
|
||||
}
|
||||
|
||||
defstruct user_agent: "change me",
|
||||
req_options: []
|
||||
|
||||
@schema NimbleOptions.new!(
|
||||
user_agent: [
|
||||
type: :string,
|
||||
required: false,
|
||||
default: "change me"
|
||||
],
|
||||
req_options: [
|
||||
type: :keyword_list,
|
||||
required: false,
|
||||
default: []
|
||||
]
|
||||
)
|
||||
|
||||
@doc NimbleOptions.docs(@schema)
|
||||
@spec resolve(Application.app()) :: t | no_return
|
||||
def resolve(otp_app) do
|
||||
app_config =
|
||||
Application.get_env(otp_app, Wikipedia)
|
||||
|> NimbleOptions.validate!(@schema)
|
||||
|
||||
struct(__MODULE__, app_config)
|
||||
end
|
||||
end
|
||||
@@ -1842,3 +1842,28 @@ msgstr ""
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Error refreshing Wikipedia data"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Read full article on Wikipedia"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Refresh Wikipedia"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Wikipedia data"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.ex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Wikipedia data refreshed successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1842,3 +1842,28 @@ msgstr ""
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Set deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Error refreshing Wikipedia data"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Read full article on Wikipedia"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Refresh Wikipedia"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.html.heex
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Wikipedia data"
|
||||
msgstr ""
|
||||
|
||||
#: lib/music_library_web/live/artist_live/show.ex
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Wikipedia data refreshed successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule MusicLibrary.Repo.Migrations.AddWikipediaDataToArtistInfos do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:artist_infos) do
|
||||
add :wikipedia_data, :map
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
defmodule MusicBrainz.ArtistTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MusicBrainz.Artist
|
||||
|
||||
describe "get_wikidata_id/1" do
|
||||
test "extracts wikidata ID from relations" do
|
||||
artist_data = MusicBrainz.Fixtures.Artist.get_artist()
|
||||
artist = Artist.from_api_response(artist_data)
|
||||
|
||||
assert Artist.get_wikidata_id(artist) == "Q352766"
|
||||
end
|
||||
|
||||
test "returns nil when no wikidata relation exists" do
|
||||
artist = %Artist{
|
||||
id: "test",
|
||||
name: "Test",
|
||||
sort_name: "Test",
|
||||
relations: [
|
||||
%{type: "discogs", url: %{"resource" => "https://www.discogs.com/artist/123"}}
|
||||
]
|
||||
}
|
||||
|
||||
assert Artist.get_wikidata_id(artist) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"batchcomplete": "",
|
||||
"query": {
|
||||
"pages": {
|
||||
"1845781": {
|
||||
"pageid": 1845781,
|
||||
"ns": 0,
|
||||
"title": "Steven Wilson",
|
||||
"extract": "<p><b>Steven John Wilson</b> (born 3 November 1967) is an English musician. He is most associated with the progressive rock genre, though his influences and work extend beyond it. Wilson first became known as the founder, lead guitarist, singer, and songwriter of the band Porcupine Tree, as well as being a member of several other bands.</p>\n<p>Wilson has been involved in musical projects including No-Man, Bass Communion, Blackfield, and Storm Corrosion. He has also worked as a record producer and is known for his surround sound and high-fidelity stereo remixes of classic albums by artists such as King Crimson, Jethro Tull, Tears for Fears, Roxy Music, and Yes.</p>\n<p>His solo career began in earnest in 2008. He has released six solo studio albums, including <i>The Raven That Refused to Sing (And Other Stories)</i> (2013) and <i>Hand. Cannot. Erase.</i> (2015), both of which received critical acclaim. His seventh album, <i>The Overview</i>, was released in 2025.</p>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "standard",
|
||||
"title": "Steven Wilson",
|
||||
"displaytitle": "Steven Wilson",
|
||||
"namespace": {
|
||||
"id": 0,
|
||||
"text": ""
|
||||
},
|
||||
"wikibase_item": "Q352766",
|
||||
"titles": {
|
||||
"canonical": "Steven_Wilson",
|
||||
"normalized": "Steven Wilson",
|
||||
"display": "Steven Wilson"
|
||||
},
|
||||
"pageid": 1845781,
|
||||
"extract": "Steven John Wilson is an English musician. He is most associated with the progressive rock genre, though his influences and work extend beyond it.",
|
||||
"extract_html": "<p><b>Steven John Wilson</b> is an English musician. He is most associated with the progressive rock genre, though his influences and work extend beyond it.</p>",
|
||||
"description": "English musician and record producer",
|
||||
"content_urls": {
|
||||
"desktop": {
|
||||
"page": "https://en.wikipedia.org/wiki/Steven_Wilson",
|
||||
"revisions": "https://en.wikipedia.org/wiki/Steven_Wilson?action=history",
|
||||
"edit": "https://en.wikipedia.org/wiki/Steven_Wilson?action=edit",
|
||||
"talk": "https://en.wikipedia.org/wiki/Talk:Steven_Wilson"
|
||||
},
|
||||
"mobile": {
|
||||
"page": "https://en.m.wikipedia.org/wiki/Steven_Wilson",
|
||||
"revisions": "https://en.m.wikipedia.org/w/index.php?title=Steven_Wilson&action=history",
|
||||
"edit": "https://en.m.wikipedia.org/w/index.php?title=Steven_Wilson&action=edit",
|
||||
"talk": "https://en.m.wikipedia.org/wiki/Talk:Steven_Wilson"
|
||||
}
|
||||
},
|
||||
"lang": "en",
|
||||
"dir": "ltr"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
defmodule Wikipedia.Fixtures do
|
||||
@fixtures_folder Path.join([File.cwd!(), "test/support/fixtures/wikipedia"])
|
||||
|
||||
def wikidata_response do
|
||||
Path.join([@fixtures_folder, "wikidata_response.json"])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
end
|
||||
|
||||
def wikidata_response_no_enwiki do
|
||||
Path.join([@fixtures_folder, "wikidata_response_no_enwiki.json"])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
end
|
||||
|
||||
def article_summary do
|
||||
Path.join([@fixtures_folder, "article_summary.json"])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
end
|
||||
|
||||
def article_extract do
|
||||
Path.join([@fixtures_folder, "article_extract.json"])
|
||||
|> File.read!()
|
||||
|> JSON.decode!()
|
||||
end
|
||||
|
||||
def article_extract_html do
|
||||
article_extract()
|
||||
|> get_in(["query", "pages"])
|
||||
|> Map.values()
|
||||
|> List.first()
|
||||
|> Map.get("extract")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"entities": {
|
||||
"Q352766": {
|
||||
"type": "item",
|
||||
"id": "Q352766",
|
||||
"sitelinks": {
|
||||
"enwiki": {
|
||||
"site": "enwiki",
|
||||
"title": "Steven Wilson",
|
||||
"badges": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"success": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"entities": {
|
||||
"Q999999": {
|
||||
"type": "item",
|
||||
"id": "Q999999",
|
||||
"sitelinks": {}
|
||||
}
|
||||
},
|
||||
"success": 1
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
defmodule WikipediaTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
describe "get_artist_summary/1" do
|
||||
test "resolves wikidata ID to Wikipedia summary with full intro" do
|
||||
wikidata_id = "Q352766"
|
||||
summary = Wikipedia.Fixtures.article_summary()
|
||||
intro_html = Wikipedia.Fixtures.article_extract_html()
|
||||
|
||||
Req.Test.stub(Wikipedia.API, fn conn ->
|
||||
case conn.request_path do
|
||||
"/w/api.php" ->
|
||||
case conn.params["action"] do
|
||||
"wbgetentities" ->
|
||||
Req.Test.json(conn, Wikipedia.Fixtures.wikidata_response())
|
||||
|
||||
"query" ->
|
||||
Req.Test.json(conn, Wikipedia.Fixtures.article_extract())
|
||||
end
|
||||
|
||||
"/api/rest_v1/page/summary/Steven%20Wilson" ->
|
||||
Req.Test.json(conn, summary)
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, result} = Wikipedia.get_artist_summary(wikidata_id)
|
||||
assert result["extract"] == summary["extract"]
|
||||
assert result["description"] == summary["description"]
|
||||
assert result["intro_html"] == intro_html
|
||||
end
|
||||
|
||||
test "returns error when no English Wikipedia article exists" do
|
||||
wikidata_id = "Q999999"
|
||||
|
||||
Req.Test.stub(Wikipedia.API, fn conn ->
|
||||
Req.Test.json(conn, Wikipedia.Fixtures.wikidata_response_no_enwiki())
|
||||
end)
|
||||
|
||||
assert {:error, :no_english_wikipedia} = Wikipedia.get_artist_summary(wikidata_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user