diff --git a/lib/music_library/records/importer.ex b/lib/music_library/records/importer.ex index ee519344..c593837a 100644 --- a/lib/music_library/records/importer.ex +++ b/lib/music_library/records/importer.ex @@ -1,5 +1,32 @@ defmodule MusicLibrary.Records.Importer do - @moduledoc """ + require Logger + import Ecto.Query, warn: false + + alias MusicLibrary.Records.Record, as: Rec + alias MusicLibrary.Records.MusicBrainz + alias MusicLibrary.Repo + + def import_all_artists do + Rec + |> Repo.all() + |> Enum.each(fn r -> + import_artists(r) + Process.sleep(1000) + end) + end + + def import_missing_artists do + q = from(r in Rec, where: is_nil(r.artists)) + + q + |> Repo.all() + |> Enum.each(fn r -> + import_artists(r) + Process.sleep(1000) + end) + end + + @doc """ The original data from Obsidian maps records to release groups, so to find artists for a record we can use the [lookup](https://musicbrainz.org/doc/MusicBrainz_API#Lookups) endpoint with the release group id and include the artist credits. @@ -33,13 +60,6 @@ defmodule MusicLibrary.Records.Importer do ] } """ - - require Logger - import Ecto.Query, warn: false - - alias MusicLibrary.Records.Record, as: Rec - alias MusicLibrary.Records.MusicBrainz - def import_artists(record) do with {:ok, data} <- MusicBrainz.get_release_group(record.musicbrainz_id) do artists_attrs = @@ -56,10 +76,14 @@ defmodule MusicLibrary.Records.Importer do record |> Rec.add_artists(artists_attrs) - |> MusicLibrary.Repo.update!() + |> Repo.update!() end end + @doc """ + Pull the cover image from the stored url and keep a local, resized copy in + the database for fast access/use. + """ def import_cover_image(record) do with {:ok, image_data} <- blob_get(record.image_url) do {:ok, thumb} = Vix.Vips.Operation.thumbnail_buffer(image_data, 400) @@ -67,22 +91,25 @@ defmodule MusicLibrary.Records.Importer do record |> Rec.add_image_data(thumb_data) - |> MusicLibrary.Repo.update!() + |> Repo.update!() end end + @doc """ + Given an already stored image in the database, resize it to a 400px wide thumbnail. + """ def resize_cover_image(record) do {:ok, thumb} = Vix.Vips.Operation.thumbnail_buffer(record.image_data, 400) {:ok, thumb_data} = Vix.Vips.Image.write_to_buffer(thumb, ".jpg") record |> Rec.add_image_data(thumb_data) - |> MusicLibrary.Repo.update!() + |> Repo.update!() end def import_all_cover_images do Rec - |> MusicLibrary.Repo.all() + |> Repo.all() |> Enum.each(fn r -> if r.image_data == nil do import_cover_image(r) @@ -93,7 +120,7 @@ defmodule MusicLibrary.Records.Importer do def resize_all_cover_images do Rec - |> MusicLibrary.Repo.all() + |> Repo.all() |> Enum.each(fn r -> if r.image_data != nil do resize_cover_image(r) @@ -102,38 +129,21 @@ defmodule MusicLibrary.Records.Importer do end) end - def import_all do - Rec - |> MusicLibrary.Repo.all() - |> Enum.each(fn r -> - import_artists(r) - Process.sleep(1000) - end) - end - - def import_missing do - q = from(r in Rec, where: is_nil(r.artists)) - - q - |> MusicLibrary.Repo.all() - |> Enum.each(fn r -> - import_artists(r) - Process.sleep(1000) - 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 -> diff --git a/lib/music_library/records/music_brainz.ex b/lib/music_library/records/music_brainz.ex index 0bd85da4..90b692b4 100644 --- a/lib/music_library/records/music_brainz.ex +++ b/lib/music_library/records/music_brainz.ex @@ -1,6 +1,46 @@ defmodule MusicLibrary.Records.MusicBrainz do + @moduledoc """ + The original data from Obsidian maps records to MusicBrainz release groups, so we can leverage the MusicBrainz API to: + + - Import new records + - Extend the metadata associated with existing records + """ + require Logger + @doc """ + Uses the [lookup](https://musicbrainz.org/doc/MusicBrainz_API#Lookups) endpoint with the release group id and include the + artist credits. + + Example request: https://musicbrainz.org/ws/2/release-group/ae504fd6-8498-463e-8d96-14f9e11d1863?fmt=json&inc=artist-credits + + Example response: + + { + "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", + "id": "ae504fd6-8498-463e-8d96-14f9e11d1863", + "primary-type": "Album", + "secondary-types": [], + "disambiguation": "", + "title": "Dwellers of the Deep", + "secondary-type-ids": [], + "first-release-date": "2020-10-23", + "artist-credit": [ + { + "artist": { + "type-id": "e431f5f6-b5d2-343d-8b36-72607fffb74b", + "sort-name": "Wobbler", + "id": "923b9160-251f-4ebe-8af2-ae670c425e55", + "type": "Group", + "name": "Wobbler", + "disambiguation": "Symphonic Prog, Norway" + }, + "name": "Wobbler", + "joinphrase": "" + } + ] + } + """ def get_release_group(id) do url = "https://musicbrainz.org/ws/2/release-group/#{id}?fmt=json&inc=artist-credits" @@ -8,6 +48,106 @@ defmodule MusicLibrary.Records.MusicBrainz do json_get(url) end + @doc """ + Uses the [search](https://musicbrainz.org/doc/MusicBrainz_API/Search#Release_Group) endpoint with a search query string. + + Note that the returned release groups are different from a lookup result + because they don't include **genres** and **cover image**. + + Example request: https://musicbrainz.org/ws/2/release-group?query=marbles&limit=20&offset=0&fmt=json + + Example response: + + { + "created": "2024-09-23T08:21:24.310Z", + "count": 2, + "offset": 0, + "release-groups": [ + { + "id": "0b6813e2-8524-4c09-9a57-8dcb9e985f8d", + "type-id": "6d0c5bf6-7a33-3420-a519-44fc63eedebf", + "score": 100, + "primary-type-id": "6d0c5bf6-7a33-3420-a519-44fc63eedebf", + "count": 1, + "title": "Eupnea", + "first-release-date": "2021-05-28", + "primary-type": "EP", + "artist-credit": [ + { + "name": "Ray of Dreams", + "artist": { + "id": "e66e261a-0b52-4801-bbe4-b81c0f157bfe", + "name": "Ray of Dreams", + "sort-name": "Ray of Dreams" + } + } + ], + "releases": [ + { + "id": "b1507d2b-e96a-4838-96d5-c11698bec764", + "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", + "title": "Eupnea", + "status": "Official" + } + ] + }, + { + "id": "0914b820-6303-4fd6-9bf8-c4662669fe43", + "type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", + "score": 100, + "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", + "count": 3, + "title": "Eupnea", + "first-release-date": "2020-04-03", + "primary-type": "Album", + "artist-credit": [ + { + "name": "Pure Reason Revolution", + "artist": { + "id": "f443a331-2623-4d50-8797-bfb204850253", + "name": "Pure Reason Revolution", + "sort-name": "Pure Reason Revolution" + } + } + ], + "releases": [ + { + "id": "3cd333ef-0300-4536-afd7-a23848f01c1e", + "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", + "title": "Eupnea", + "status": "Official" + }, + { + "id": "b5da9e78-f174-4d56-b76a-1fa47718e9a4", + "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", + "title": "Eupnea", + "status": "Official" + }, + { + "id": "4a374adf-42af-442b-aec0-c9b0da10f32e", + "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", + "title": "Eupnea", + "status": "Official" + } + ], + "tags": [ + { + "count": 1, + "name": "rock" + }, + { + "count": 1, + "name": "electronic" + }, + { + "count": 1, + "name": "progressive rock" + } + ] + } + ] + } + """ def search_release_group(query, opts \\ []) do limit = Keyword.get(opts, :limit, 20) offset = Keyword.get(opts, :offset, 0) @@ -31,6 +171,8 @@ defmodule MusicLibrary.Records.MusicBrainz do {"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, Jason.decode!(response.body)}