Files
music_library/lib/music_brainz/api_impl.ex
T
Claudio Ortolina f29dd1f0ab Fetch 100 releases when importing a record
The release group contains only 25 releases, which breaks tracking
scrobble status. Albums like "Toto IV" or "Hounds of Love" end up
looking like they're not part of the collections because the individual
releases are not stored.

Still error prone, because this change doesn't paginate to more than 100
records, but that will be addressed in a future commit.
2025-02-05 10:12:27 +00:00

490 lines
15 KiB
Elixir

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
associated artists, genres, releases and release group relations. Note that the API limits each included resource to 25 items.
> Note that the number of linked entities returned is always limited to 25. If you need the remaining results, you will have to perform a browse request.
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, config) do
url =
"https://musicbrainz.org/ws/2/release-group/#{id}?fmt=json&inc=artists+genres+releases+release-group-rels"
json_get(url, config)
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, config) do
url =
"https://musicbrainz.org/ws/2/release/#{id}?fmt=json&inc=release-groups"
json_get(url, config)
end
@impl true
def get_releases(release_group_id, config) do
url =
"https://musicbrainz.org/ws/2/release?fmt=json&limit=100&release-group=#{release_group_id}"
json_get(url, config)
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, config) 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, config) 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}, config) do
url = "https://coverartarchive.org/release-group/#{musicbrainz_id}/front"
get_cover_art({:url, url}, config)
end
def get_cover_art({:url, url}, config) do
case blob_get(url, config) do
{:error, reason} ->
Logger.error("Failed to fetch cover art for #{url}, reason: #{inspect(reason)}")
{:error, :cover_not_available}
success ->
success
end
end
defp json_get(url, config) do
req =
Finch.build(:get, url, [
{"User-Agent", config.user_agent}
])
Logger.debug("Fetching data from #{url}")
case Finch.request(req, MusicBrainz.Finch) do
{:ok, response} when response.status == 200 ->
{:ok, JSON.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, config)
other ->
msg = "Failed to fetch data from #{url}, reason: #{inspect(other)}"
Logger.error(msg)
{:error, msg}
end
end
defp blob_get(url, config) do
req =
Finch.build(:get, url, [
{"User-Agent", config.user_agent}
])
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, config)
# all non-success responses can be treated as errors
{:ok, response} ->
{:error, response}
error ->
error
end
end
end