Support getting artist bios from Wikipedia (experimental)

This commit is contained in:
Claudio Ortolina
2026-02-09 15:19:35 +00:00
parent f82e764c94
commit bcae291cff
23 changed files with 639 additions and 31 deletions
+3 -1
View File
@@ -35,7 +35,9 @@
"Bash(mix compile:*)", "Bash(mix compile:*)",
"mcp__tidewave__get_docs", "mcp__tidewave__get_docs",
"mcp__tidewave__search_package_docs", "mcp__tidewave__search_package_docs",
"Bash(wc:*)" "Bash(wc:*)",
"Bash(python3:*)",
"Bash(MIX_ENV=test mix ecto.migrate:*)"
] ]
}, },
"enableAllProjectMcpServers": false "enableAllProjectMcpServers": false
+3 -1
View File
@@ -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, Discogs, personal_access_token: "change me", user_agent: user_agent
config :music_library, Wikipedia, user_agent: user_agent
# Configure esbuild (the version is required) # Configure esbuild (the version is required)
config :esbuild, config :esbuild,
version: "0.27.3", version: "0.27.3",
@@ -78,7 +80,7 @@ config :phoenix, :json_library, JSON
config :music_library, Oban, config :music_library, Oban,
engine: Oban.Engines.Lite, 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, repo: MusicLibrary.BackgroundRepo,
plugins: [ plugins: [
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
+6
View File
@@ -57,6 +57,12 @@ config :music_library, Discogs,
max_retries: 0 max_retries: 0
] ]
config :music_library, Wikipedia,
req_options: [
plug: {Req.Test, Wikipedia.API},
max_retries: 0
]
config :phoenix_test, :endpoint, MusicLibraryWeb.Endpoint config :phoenix_test, :endpoint, MusicLibraryWeb.Endpoint
config :music_library, Oban, testing: :manual config :music_library, Oban, testing: :manual
+11
View File
@@ -32,6 +32,14 @@ defmodule MusicBrainz.Artist do
end end
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 def url(id) do
"https://musicbrainz.org/artist/#{id}" "https://musicbrainz.org/artist/#{id}"
end 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("https://www.discogs.com/artist/" <> id), do: String.to_integer(id)
defp parse_discogs_id(_other), do: nil 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 end
+26
View File
@@ -134,6 +134,32 @@ defmodule MusicLibrary.Artists do
enqueue_worker(Worker.ArtistRefreshDiscogsData, %{"id" => artist_info.id}) enqueue_worker(Worker.ArtistRefreshDiscogsData, %{"id" => artist_info.id})
end 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 def create_artist_info(attrs) do
%ArtistInfo{} %ArtistInfo{}
|> ArtistInfo.changeset(attrs) |> ArtistInfo.changeset(attrs)
+31
View File
@@ -10,6 +10,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
schema "artist_infos" do schema "artist_infos" do
field :musicbrainz_data, :map, default: %{} field :musicbrainz_data, :map, default: %{}
field :discogs_data, :map, default: %{} field :discogs_data, :map, default: %{}
field :wikipedia_data, :map, default: %{}
field :image_data_hash, :string field :image_data_hash, :string
has_one :note, Note, foreign_key: :musicbrainz_id has_one :note, Note, foreign_key: :musicbrainz_id
@@ -23,6 +24,7 @@ defmodule MusicLibrary.Artists.ArtistInfo do
:id, :id,
:musicbrainz_data, :musicbrainz_data,
:discogs_data, :discogs_data,
:wikipedia_data,
:image_data_hash :image_data_hash
]) ])
|> validate_required([:musicbrainz_data]) |> validate_required([:musicbrainz_data])
@@ -82,4 +84,33 @@ defmodule MusicLibrary.Artists.ArtistInfo do
_ -> nil _ -> nil
end end
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 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 @impl Oban.Worker
def perform(%Oban.Job{args: %{"id" => artist_id}}) do def perform(%Oban.Job{args: %{"id" => artist_id}}) do
with {:ok, _artist_info} <- MusicLibrary.Artists.fetch_artist_info(artist_id), 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, _artist_info} <- MusicLibrary.Artists.fetch_image(artist_id) do
:ok :ok
end end
@@ -101,6 +101,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
{:noreply, {:noreply,
socket socket
|> assign(:artist_info, artist_info) |> assign(:artist_info, artist_info)
|> assign(:biography, build_biography(artist_info))
|> put_toast(:info, gettext("Artist info refreshed successfully"))} |> put_toast(:info, gettext("Artist info refreshed successfully"))}
{:error, reason} -> {:error, reason} ->
@@ -113,6 +114,25 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
end end
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 def handle_event("refresh_artist_image", %{"id" => id}, socket) do
case Artists.fetch_image(id) do case Artists.fetch_image(id) do
{:ok, artist_info} -> {:ok, artist_info} ->
@@ -171,6 +191,7 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
|> assign(:current_section, :artists) |> assign(:current_section, :artists)
|> assign(:artist, artist) |> assign(:artist, artist)
|> assign(:artist_info, artist_info) |> assign(:artist_info, artist_info)
|> assign(:biography, build_biography(artist_info))
|> assign(:external_links, ArtistInfo.external_links(artist_info)) |> assign(:external_links, ArtistInfo.external_links(artist_info))
|> assign(:country, ArtistInfo.country(artist_info)) |> assign(:country, ArtistInfo.country(artist_info))
|> assign_async(:lastfm_artist_info, fn -> |> assign_async(:lastfm_artist_info, fn ->
@@ -250,6 +271,20 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
} }
end 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. # 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. # We split the bio at the read more link in order to render the license separately.
defp render_bio(bio) do defp render_bio(bio) do
@@ -73,6 +73,20 @@
/> />
{gettext("Refresh info")} {gettext("Refresh info")}
</.dropdown_link> </.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> </.focus_wrap>
</.dropdown> </.dropdown>
</.button_group> </.button_group>
@@ -141,34 +155,19 @@
/> />
</dd> </dd>
</dl> </dl>
<.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}> <%= if @biography do %>
<:loading> <dt class="mt-4 text-sm font-medium leading-6 text-zinc-900 dark:text-zinc-400">
<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")} {gettext("Biography")}
<.badge variant="soft" class="ml-1">{@biography.source}</.badge>
</dt> </dt>
<dd <dd class="text-zinc-700 dark:text-zinc-300">
:if={lastfm_artist_info.bio not in [nil, ""]} <p
class="text-zinc-700 dark:text-zinc-300" :if={@biography.description}
> class="mt-2 text-sm italic text-zinc-500 dark:text-zinc-400"
{remove_read_more_link(lastfm_artist_info.summary)} >
{@biography.description}
</p>
<p class="mt-2 text-sm/7">{@biography.summary_html}</p>
<.link <.link
class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400" class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
phx-click={Fluxon.open_dialog("bio")} phx-click={Fluxon.open_dialog("bio")}
@@ -183,14 +182,80 @@
</.link> </.link>
</dd> </dd>
<.sheet <.sheet
:if={lastfm_artist_info.bio not in [nil, ""]}
id="bio" id="bio"
class="max-w-2xl text-zinc-700 dark:text-zinc-300" class="max-w-2xl text-zinc-700 dark:text-zinc-300"
placement="left" 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> </.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} /> <.external_links external_links={@external_links} />
</div> </div>
<div class="md:col-span-7 md:order-1"> <div class="md:col-span-7 md:order-1">
@@ -251,6 +316,12 @@
title={gettext("Discogs data")} title={gettext("Discogs data")}
data={@artist_info.discogs_data} data={@artist_info.discogs_data}
/> />
<.json_viewer
:if={@artist_info.wikipedia_data != %{}}
title={gettext("Wikipedia data")}
data={@artist_info.wikipedia_data}
/>
</div> </div>
<.live_component <.live_component
+18
View File
@@ -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
+124
View File
@@ -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
+32
View File
@@ -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
+25
View File
@@ -1842,3 +1842,28 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Set deleted successfully" msgid "Set deleted successfully"
msgstr "" 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 ""
+25
View File
@@ -1842,3 +1842,28 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Set deleted successfully" msgid "Set deleted successfully"
msgstr "" 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
+27
View File
@@ -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
}
+42
View File
@@ -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