diff --git a/lib/music_library/records/music_brainz.ex b/lib/music_library/records/music_brainz.ex index 90b692b4..8ee35c0b 100644 --- a/lib/music_library/records/music_brainz.ex +++ b/lib/music_library/records/music_brainz.ex @@ -43,7 +43,7 @@ defmodule MusicLibrary.Records.MusicBrainz do """ def get_release_group(id) do 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) end @@ -165,6 +165,15 @@ defmodule MusicLibrary.Records.MusicBrainz do json_get(url) 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 req = Finch.build(:get, url, [ @@ -183,4 +192,28 @@ defmodule MusicLibrary.Records.MusicBrainz do {:error, msg} 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 diff --git a/lib/music_library/records/record.ex b/lib/music_library/records/record.ex index e1e3390c..a255afcd 100644 --- a/lib/music_library/records/record.ex +++ b/lib/music_library/records/record.ex @@ -26,8 +26,8 @@ defmodule MusicLibrary.Records.Record do @doc false def changeset(record, attrs) do record - |> cast(attrs, [:type, :title, :musicbrainz_id, :year, :genres, :image_url]) - |> validate_required([: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]) end def add_artists(record, artists_attrs) do diff --git a/lib/music_library_web/live/record_live/index.ex b/lib/music_library_web/live/record_live/index.ex index ab2b8e28..7bdb63a9 100644 --- a/lib/music_library_web/live/record_live/index.ex +++ b/lib/music_library_web/live/record_live/index.ex @@ -24,6 +24,12 @@ defmodule MusicLibraryWeb.RecordLive.Index do {:noreply, apply_action(socket, socket.assigns.live_action, params)} 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 socket |> assign(:page_title, "Edit Record") @@ -62,6 +68,10 @@ defmodule MusicLibraryWeb.RecordLive.Index do {:noreply, stream_insert(socket, :records, record)} end + def handle_info({__MODULE__, {:saved, record}}, socket) do + {:noreply, push_navigate(socket, to: ~p"/records/#{record}")} + end + @impl true def handle_event("delete", %{"id" => id}, socket) do record = Records.get_record!(id) @@ -70,6 +80,70 @@ defmodule MusicLibraryWeb.RecordLive.Index do {:noreply, stream_delete(socket, :records, record)} 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 "https://musicbrainz.org/release-group/#{record.musicbrainz_id}" end diff --git a/lib/music_library_web/live/record_live/index.html.heex b/lib/music_library_web/live/record_live/index.html.heex index 398a405a..1889fd0f 100644 --- a/lib/music_library_web/live/record_live/index.html.heex +++ b/lib/music_library_web/live/record_live/index.html.heex @@ -4,6 +4,9 @@ <.link patch={~p"/records/new"}> <.button>New Record + <.link patch={~p"/records/search"}> + <.button>Search Record + @@ -55,4 +58,16 @@ /> +<.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="" + /> + + <.pagination pagination_params={@pagination_params} /> diff --git a/lib/music_library_web/live/record_live/search_component.ex b/lib/music_library_web/live/record_live/search_component.ex new file mode 100644 index 00000000..623a6b39 --- /dev/null +++ b/lib/music_library_web/live/record_live/search_component.ex @@ -0,0 +1,122 @@ +defmodule MusicLibraryWeb.RecordLive.SearchComponent do + use MusicLibraryWeb, :live_component + + alias MusicLibrary.Records.MusicBrainz + @impl true + def render(assigns) do + ~H""" +
+ <%= @record.title %> + + + <%= @record.year %> + +
+<%= @record.artists %>
+