Can search and import records
Requires refactor and cleanup, along with integration tests
This commit is contained in:
@@ -43,7 +43,7 @@ defmodule MusicLibrary.Records.MusicBrainz do
|
|||||||
"""
|
"""
|
||||||
def get_release_group(id) do
|
def get_release_group(id) do
|
||||||
url =
|
url =
|
||||||
"https://musicbrainz.org/ws/2/release-group/#{id}?fmt=json&inc=artist-credits"
|
"https://musicbrainz.org/ws/2/release-group/#{id}?fmt=json&inc=artist-credits+genres"
|
||||||
|
|
||||||
json_get(url)
|
json_get(url)
|
||||||
end
|
end
|
||||||
@@ -165,6 +165,15 @@ defmodule MusicLibrary.Records.MusicBrainz do
|
|||||||
json_get(url)
|
json_get(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_cover_art(musicbrainz_id) do
|
||||||
|
url = "https://coverartarchive.org/release-group/#{musicbrainz_id}/front"
|
||||||
|
|
||||||
|
with {:ok, image_data} <- blob_get(url),
|
||||||
|
{:ok, thumb} = Vix.Vips.Operation.thumbnail_buffer(image_data, 400) do
|
||||||
|
Vix.Vips.Image.write_to_buffer(thumb, ".jpg")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp json_get(url) do
|
defp json_get(url) do
|
||||||
req =
|
req =
|
||||||
Finch.build(:get, url, [
|
Finch.build(:get, url, [
|
||||||
@@ -183,4 +192,28 @@ defmodule MusicLibrary.Records.MusicBrainz do
|
|||||||
{:error, msg}
|
{:error, msg}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp blob_get(url) do
|
||||||
|
req =
|
||||||
|
Finch.build(:get, url, [
|
||||||
|
{"User-Agent", "MusicLibrary/0.1.0 ( cloud8421@gmail.com )"}
|
||||||
|
])
|
||||||
|
|
||||||
|
Logger.debug("Fetching data from #{url}")
|
||||||
|
|
||||||
|
case Finch.request(req, MusicLibrary.Finch) do
|
||||||
|
{:ok, response} when response.status == 200 ->
|
||||||
|
{:ok, response.body}
|
||||||
|
|
||||||
|
{:ok, response} when response.status in 301..308 ->
|
||||||
|
location = :proplists.get_value("location", response.headers)
|
||||||
|
Logger.debug("Following redirect to #{location}")
|
||||||
|
blob_get(location)
|
||||||
|
|
||||||
|
other ->
|
||||||
|
msg = "Failed to fetch data from #{url}, reason: #{inspect(other)}"
|
||||||
|
Logger.error(msg)
|
||||||
|
{:error, msg}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ defmodule MusicLibrary.Records.Record do
|
|||||||
@doc false
|
@doc false
|
||||||
def changeset(record, attrs) do
|
def changeset(record, attrs) do
|
||||||
record
|
record
|
||||||
|> cast(attrs, [:type, :title, :musicbrainz_id, :year, :genres, :image_url])
|
|> cast(attrs, [:type, :title, :musicbrainz_id, :year, :genres, :image_url, :image_data])
|
||||||
|> validate_required([:type, :title, :musicbrainz_id, :year, :genres, :image_url])
|
|> validate_required([:type, :title, :musicbrainz_id, :year, :genres])
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_artists(record, artists_attrs) do
|
def add_artists(record, artists_attrs) do
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ defmodule MusicLibraryWeb.RecordLive.Index do
|
|||||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp apply_action(socket, :search, _params) do
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Search Records")
|
||||||
|
|> assign(:record, nil)
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Edit Record")
|
|> assign(:page_title, "Edit Record")
|
||||||
@@ -62,6 +68,10 @@ defmodule MusicLibraryWeb.RecordLive.Index do
|
|||||||
{:noreply, stream_insert(socket, :records, record)}
|
{:noreply, stream_insert(socket, :records, record)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({__MODULE__, {:saved, record}}, socket) do
|
||||||
|
{:noreply, push_navigate(socket, to: ~p"/records/#{record}")}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
record = Records.get_record!(id)
|
record = Records.get_record!(id)
|
||||||
@@ -70,6 +80,70 @@ defmodule MusicLibraryWeb.RecordLive.Index do
|
|||||||
{:noreply, stream_delete(socket, :records, record)}
|
{:noreply, stream_delete(socket, :records, record)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("import", %{"id" => musicbrainz_id}, socket) do
|
||||||
|
with {:ok, result} <- MusicLibrary.Records.MusicBrainz.get_release_group(musicbrainz_id),
|
||||||
|
{:ok, image_data} <- MusicLibrary.Records.MusicBrainz.get_cover_art(musicbrainz_id) do
|
||||||
|
artists_attrs =
|
||||||
|
result
|
||||||
|
|> get_in(["artist-credit", Access.all(), "artist"])
|
||||||
|
|> Enum.map(fn artist ->
|
||||||
|
%{
|
||||||
|
name: artist["name"],
|
||||||
|
musicbrainz_id: artist["id"],
|
||||||
|
sort_name: artist["sort-name"],
|
||||||
|
disambiguation: artist["disambiguation"]
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
record_attrs = %{
|
||||||
|
"musicbrainz_id" => musicbrainz_id,
|
||||||
|
"title" => result["title"],
|
||||||
|
"artists" => artists_attrs,
|
||||||
|
"year" => parse_year(result["first-release-date"]),
|
||||||
|
"type" => parse_subtype(result["primary-type"]),
|
||||||
|
"genres" => Enum.map(result["genres"], fn g -> g["name"] end),
|
||||||
|
"image_url" => "https://coverartarchive.org/release-group/#{musicbrainz_id}/front",
|
||||||
|
"image_data" => image_data
|
||||||
|
}
|
||||||
|
|
||||||
|
case Records.create_record(record_attrs) do
|
||||||
|
{:ok, record} ->
|
||||||
|
notify_parent({:saved, record})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Record imported successfully")
|
||||||
|
|> push_patch(to: ~p"/records")}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:error, _} ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||||
|
|
||||||
|
defp parse_year(iso_date) when is_binary(iso_date) do
|
||||||
|
case Date.from_iso8601(iso_date) do
|
||||||
|
{:ok, date} ->
|
||||||
|
date.year
|
||||||
|
|
||||||
|
_error ->
|
||||||
|
{year, _rest} = Integer.parse(iso_date)
|
||||||
|
{:ok, year}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_subtype("Album"), do: :album
|
||||||
|
defp parse_subtype("EP"), do: :ep
|
||||||
|
defp parse_subtype("Live"), do: :live
|
||||||
|
defp parse_subtype("Compilation"), do: :compilation
|
||||||
|
defp parse_subtype("Single"), do: :single
|
||||||
|
defp parse_subtype(_), do: :other
|
||||||
|
|
||||||
defp musicbrainz_url(record) do
|
defp musicbrainz_url(record) do
|
||||||
"https://musicbrainz.org/release-group/#{record.musicbrainz_id}"
|
"https://musicbrainz.org/release-group/#{record.musicbrainz_id}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<.link patch={~p"/records/new"}>
|
<.link patch={~p"/records/new"}>
|
||||||
<.button>New Record</.button>
|
<.button>New Record</.button>
|
||||||
</.link>
|
</.link>
|
||||||
|
<.link patch={~p"/records/search"}>
|
||||||
|
<.button>Search Record</.button>
|
||||||
|
</.link>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
@@ -55,4 +58,16 @@
|
|||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|
||||||
|
<.modal :if={@live_action == :search} id="record-modal" show on_cancel={JS.patch(~p"/records")}>
|
||||||
|
<.live_component
|
||||||
|
module={MusicLibraryWeb.RecordLive.SearchComponent}
|
||||||
|
id={:search}
|
||||||
|
title={@page_title}
|
||||||
|
action={@live_action}
|
||||||
|
record={@record}
|
||||||
|
patch={~p"/records"}
|
||||||
|
initial_query=""
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
|
|
||||||
<.pagination pagination_params={@pagination_params} />
|
<.pagination pagination_params={@pagination_params} />
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
defmodule MusicLibraryWeb.RecordLive.SearchComponent do
|
||||||
|
use MusicLibraryWeb, :live_component
|
||||||
|
|
||||||
|
alias MusicLibrary.Records.MusicBrainz
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.simple_form
|
||||||
|
for={@form}
|
||||||
|
id="search-form"
|
||||||
|
phx-target={@myself}
|
||||||
|
phx-change="search"
|
||||||
|
phx-submit="search"
|
||||||
|
>
|
||||||
|
<.input
|
||||||
|
field={@form[:query]}
|
||||||
|
type="text"
|
||||||
|
label="Search"
|
||||||
|
prompt="Search for records"
|
||||||
|
phx-debounce="500"
|
||||||
|
/>
|
||||||
|
</.simple_form>
|
||||||
|
<ul role="list" class="divide-y divide-gray-100">
|
||||||
|
<.result :for={record <- @records} record={record} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp result(assigns) do
|
||||||
|
~H"""
|
||||||
|
<li
|
||||||
|
class="flex justify-between gap-x-6 py-5 cursor-pointer hover:bg-gray-50"
|
||||||
|
phx-click={JS.push("import", value: %{id: @record.id})}
|
||||||
|
data-confirm="Are you sure you want to import this record?"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 gap-x-4">
|
||||||
|
<div class="min-w-0 flex-auto">
|
||||||
|
<p class="text-sm font-semibold leading-6 text-gray-900">
|
||||||
|
<%= @record.title %>
|
||||||
|
|
||||||
|
<span class="mt-1 text-xs leading-5 text-gray-500">
|
||||||
|
<%= @record.year %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 truncate text-xs leading-5 text-gray-500"><%= @record.artists %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
|
||||||
|
<.type_badge type={@record.type} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :type, :string, required: true
|
||||||
|
|
||||||
|
defp type_badge(assigns) do
|
||||||
|
~H"""
|
||||||
|
<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
|
||||||
|
<%= @type %>
|
||||||
|
</span>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:records, [])
|
||||||
|
|> assign(:form, to_form(%{"query" => ""}))}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("search", %{"query" => query}, socket) do
|
||||||
|
{:ok, records} = search(query)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:records, records)
|
||||||
|
|> assign(:form, to_form(%{"query" => query}))}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp search(""), do: {:ok, []}
|
||||||
|
|
||||||
|
defp search(query) do
|
||||||
|
case MusicBrainz.search_release_group(query) do
|
||||||
|
{:ok, result} ->
|
||||||
|
{:ok,
|
||||||
|
Enum.map(result["release-groups"], fn rg ->
|
||||||
|
%{
|
||||||
|
id: rg["id"],
|
||||||
|
type: parse_subtype(rg["primary-type"]),
|
||||||
|
title: rg["title"],
|
||||||
|
artists:
|
||||||
|
rg["artist-credit"]
|
||||||
|
|> Enum.map(fn ac -> ac["artist"]["name"] end)
|
||||||
|
|> Enum.join(", "),
|
||||||
|
year: parse_year(rg["first-release-date"])
|
||||||
|
}
|
||||||
|
end)}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_year(iso_date) do
|
||||||
|
case Date.from_iso8601(iso_date) do
|
||||||
|
{:ok, date} -> date.year
|
||||||
|
_error -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_subtype("Album"), do: :album
|
||||||
|
defp parse_subtype("EP"), do: :ep
|
||||||
|
defp parse_subtype("Live"), do: :live
|
||||||
|
defp parse_subtype("Compilation"), do: :compilation
|
||||||
|
defp parse_subtype("Single"), do: :single
|
||||||
|
defp parse_subtype(_), do: :other
|
||||||
|
end
|
||||||
@@ -22,6 +22,7 @@ defmodule MusicLibraryWeb.Router do
|
|||||||
|
|
||||||
live "/records", RecordLive.Index, :index
|
live "/records", RecordLive.Index, :index
|
||||||
live "/records/new", RecordLive.Index, :new
|
live "/records/new", RecordLive.Index, :new
|
||||||
|
live "/records/search", RecordLive.Index, :search
|
||||||
live "/records/:id/edit", RecordLive.Index, :edit
|
live "/records/:id/edit", RecordLive.Index, :edit
|
||||||
|
|
||||||
live "/records/:id", RecordLive.Show, :show
|
live "/records/:id", RecordLive.Show, :show
|
||||||
|
|||||||
Reference in New Issue
Block a user