defmodule MusicBrainz.APIImpl 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 """ @behaviour MusicBrainz.APIBehaviour require Logger alias MusicBrainz.ReleaseGroup @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=artists+releases+genres+release-group-rels { "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", "first-release-date": "2020-10-23", "genres": [ { "count": 3, "name": "progressive rock", "disambiguation": "", "id": "ae9b8279-3959-48d8-8a88-741a7f6d4a48" }, { "count": 1, "name": "rock", "disambiguation": "", "id": "0e3fc579-2d24-4f20-9dae-736e1ec78798" }, { "id": "f729e6f8-30dc-4b81-9ff4-f4e7de82225d", "disambiguation": "", "count": 1, "name": "symphonic rock" } ], "primary-type": "Album", "secondary-type-ids": [], "releases": [ { "id": "ad9e5d8f-b8ff-4e3e-98be-f26cb8e0682e", "quality": "normal", "packaging-id": "ec27701a-4a22-37f4-bfac-6616e0f9750a", "text-representation": { "script": null, "language": null }, "date": "2020-10-23", "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", "country": "NO", "genres": [ { "name": "progressive rock", "count": 1, "id": "ae9b8279-3959-48d8-8a88-741a7f6d4a48", "disambiguation": "" } ], "title": "Dwellers of the Deep", "packaging": "Jewel Case", "release-events": [ { "area": { "iso-3166-1-codes": [ "NO" ], "type": null, "id": "6743d351-6f37-3049-9724-5041161fff4d", "sort-name": "Norway", "name": "Norway", "disambiguation": "", "type-id": null }, "date": "2020-10-23" } ], "barcode": "7090008316497", "disambiguation": "", "status": "Official" }, { "packaging-id": "119eba76-b343-3e02-a292-f0f00644bb9b", "quality": "normal", "id": "db32822c-27f6-4d1f-8775-df9513c04a9a", "date": "2020-10-23", "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", "text-representation": { "script": "Latn", "language": "eng" }, "genres": [ { "disambiguation": "", "id": "93244085-20e5-4f16-9067-1d19143b3810", "name": "classic rock", "count": 2 }, { "name": "progressive rock", "count": 2, "disambiguation": "", "id": "ae9b8279-3959-48d8-8a88-741a7f6d4a48" }, { "id": "0e3fc579-2d24-4f20-9dae-736e1ec78798", "disambiguation": "", "count": 2, "name": "rock" }, { "count": 2, "name": "symphonic rock", "id": "f729e6f8-30dc-4b81-9ff4-f4e7de82225d", "disambiguation": "" } ], "country": "XW", "status": "Official", "release-events": [ { "area": { "disambiguation": "", "type-id": null, "name": "[Worldwide]", "type": null, "sort-name": "[Worldwide]", "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", "iso-3166-1-codes": [ "XW" ] }, "date": "2020-10-23" } ], "barcode": null, "disambiguation": "", "title": "Dwellers of the Deep", "packaging": "None" }, { "id": "5715243e-9c60-49bb-a1cb-4263f065f07b", "quality": "normal", "packaging-id": "8f931351-d2e2-310f-afc6-37b89ddba246", "text-representation": { "script": "Latn", "language": "eng" }, "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", "date": "2020-10-30", "country": null, "genres": [], "barcode": "7090008311942", "release-events": [ { "date": "2020-10-30", "area": null } ], "disambiguation": "", "title": "Dwellers of the Deep", "packaging": "Digipak", "status": "Official" } ], "id": "ae504fd6-8498-463e-8d96-14f9e11d1863", "relations": [], "secondary-types": [], "title": "Dwellers of the Deep", "disambiguation": "", "artist-credit": [ { "artist": { "name": "Wobbler", "disambiguation": "symphonic prog, Norway", "type-id": "e431f5f6-b5d2-343d-8b36-72607fffb74b", "genres": [ { "count": 3, "name": "progressive rock", "disambiguation": "", "id": "ae9b8279-3959-48d8-8a88-741a7f6d4a48" }, { "name": "symphonic prog", "count": 2, "disambiguation": "", "id": "166be36f-febb-4523-a005-1fb3603bd3f6" } ], "type": "Group", "id": "923b9160-251f-4ebe-8af2-ae670c425e55", "sort-name": "Wobbler" }, "name": "Wobbler", "joinphrase": "" } ] } """ @impl true def get_release_group(id) do url = "https://musicbrainz.org/ws/2/release-group/#{id}?fmt=json&inc=artists+genres+releases+release-group-rels" json_get(url) end @doc """ Uses the [lookup](https://musicbrainz.org/doc/MusicBrainz_API#Lookups) endpoint with the release id and include the release group. Example request: https://musicbrainz.org/ws/2/release/a444b9ca-865d-4f78-a7d9-7e68999e2ca9?fmt=json&inc=release-groups Example response: { "asin": null, "barcode": null, "country": "XW", "cover-art-archive": { "artwork": true, "back": false, "count": 1, "darkened": false, "front": true }, "date": "2022-05-05", "disambiguation": "", "id": "a444b9ca-865d-4f78-a7d9-7e68999e2ca9", "packaging": null, "packaging-id": null, "quality": "normal", "release-events": [ { "area": { "disambiguation": "", "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", "iso-3166-1-codes": [ "XW" ], "name": "[Worldwide]", "sort-name": "[Worldwide]", "type": null, "type-id": null }, "date": "2022-05-05" } ], "release-group": { "disambiguation": "", "first-release-date": "2022-05-05", "id": "6916dd75-e196-4d2f-986f-345579290043", "primary-type": "Album", "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", "secondary-type-ids": [ "22a628ad-c082-3c4f-b1b6-d41665107b88" ], "secondary-types": [ "Soundtrack" ], "title": "Clark (A Dramatic Score From the Netflix Series)" }, "status": "Official", "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", "text-representation": { "language": null, "script": null }, "title": "Clark (Soundtrack From the Netflix Series)" } """ @impl true def get_release(id) do url = "https://musicbrainz.org/ws/2/release/#{id}?fmt=json&inc=release-groups" 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" } ] } ] } """ @impl true def search_release_group(query, opts) do limit = Keyword.fetch!(opts, :limit) offset = Keyword.fetch!(opts, :offset) qs = [ query: query, limit: limit, offset: offset, fmt: "json" ] url = "https://musicbrainz.org/ws/2/release-group?#{URI.encode_query(qs)}" with {:ok, result} <- json_get(url) do {:ok, Enum.map(result["release-groups"], &ReleaseGroup.from_api_response/1)} end end @doc """ Uses the [cover art](https://musicbrainz.org/doc/Cover_Art_Archive/API) endpoint with the release group id to get the cover image. """ @impl true def get_cover_art({:musicbrainz_id, musicbrainz_id}) do url = "https://coverartarchive.org/release-group/#{musicbrainz_id}/front" get_cover_art({:url, url}) end def get_cover_art({:url, url}) do with {:ok, cover_data} <- blob_get(url), {:ok, thumb} = Vix.Vips.Operation.thumbnail_buffer(cover_data, 600) do Vix.Vips.Image.write_to_buffer(thumb, ".jpg") else {:error, reason} -> Logger.error("Failed to fetch cover art for #{url}, reason: #{inspect(reason)}") {:error, :cover_not_available} end end defp json_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, MusicBrainz.Finch) do {:ok, response} when response.status == 200 -> {:ok, Jason.decode!(response.body)} {:ok, response} when response.status in 301..308 -> location = :proplists.get_value("location", response.headers) Logger.debug("Following redirect to #{location}") json_get(location) other -> msg = "Failed to fetch data from #{url}, reason: #{inspect(other)}" Logger.error(msg) {: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, MusicBrainz.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) # all non-success responses can be treated as errors {:ok, response} -> {:error, response} error -> error end end end