f29dd1f0ab
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.
490 lines
15 KiB
Elixir
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
|