Files
musiclist-server/fetcher/musicbrainz.py
2020-05-23 02:55:30 -04:00

333 lines
11 KiB
Python

"""
Modulo que se encarga de realizar requests a musicbrainz, mayormente devuelve el resultado que musicbrainz entrega
exceptuando los errores.
"""
import logging
import requests
from ratelimit import limits, sleep_and_retry
from urllib.parse import quote, urlencode
from utils import replace_key, sanitize_keys, pretty_print_json
from utils.cache import Cache as cache
_headers = {'accept': 'application/json', 'user-agent': 'MusicList/1.0 (danielcortes.xyz)'}
_mb_host = 'https://musicbrainz.org/ws/2'
_ca_host = 'http://coverartarchive.org'
_log = logging.getLogger(__name__)
_log.addHandler(logging.NullHandler())
@sleep_and_retry
@limits(calls=1, period=1)
def _do_request(url, host=_mb_host):
"""Does a request to a path and returns the dictionary representing the json response
If the response is 200 it will return the full dictionary of the json that the response
contains, and if is a 307 code, it will create a dictionary containing the response code
and the link where is redirecting, any other response code will be just appended to a
dictionary and returning.
:param str url: URL where to do the request
:return: The whole response object
:raises ValueError when user-agent isn't set
:raises ValueError when url isn't set
:returns A dictionary with the request response
it always has a status key with the status code by its value,
the rest of the response will be contained in the 'response' key
if the request didn't fail
"""
if not _headers['user-agent']:
raise ValueError('User Agent isn\'t set')
if not url:
raise ValueError('URL cant be empty')
_log.info(f'Doing request to "{url}" with headers {_headers}')
r = requests.get(url, headers=_headers)
_log.info(f'Request returned with status code {r.status_code}')
if r.status_code == 200:
response = r.json(object_hook=sanitize_keys)
elif r.status_code == 500:
response = {'status': r.status_code, 'error': f'Error del servidor'}
elif host == _ca_host:
if r.status_code == 400:
response = {'status': r.status_code, 'error': f'El mbid es invalido'}
elif r.status_code == 404:
response = {'status': r.status_code, 'error': f'No encontrado'}
elif r.status_code == 405:
response = {'status': r.status_code, 'error': f'Metodo erroneo'}
elif r.status_code == 503:
response = {'status': r.status_code, 'error': f'Rate limit exedido'}
else:
response = {'status': r.status_code, 'error': r.json()['error']}
return response
def _get(entity_type, mbid, includes=None):
"""Does a get entity query to musicbrainz
:param str entity_type: Type of the entity (artist, release, recording...)
:param str mbid: MBID of the entity to get
:param list includes: List of include parameters (recording, releases...)
:return: The JSON response
:rtype: dict
"""
if includes is None:
_log.info(f'Getting {entity_type} of mbid {mbid}')
return _do_request(f'{_mb_host}/{entity_type}/{mbid}')
else:
_log.info(f'Getting {entity_type} of mbid {mbid} including {includes}')
_includes = quote('+'.join(includes))
return _do_request(f'{_mb_host}/{entity_type}/{mbid}?inc={_includes}')
def _search(entity_type, query, includes=None, limit=25, offset=0):
"""Does a search of an entity to musicbrainz
:param str entity_type: Type of the entity (artist, release, recording...)
:param str query: The search string
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: The JSON response
:rtype: dict
"""
_log.info(f'Searching {entity_type} with query "{query}" at offset {offset} with limit of {limit} with includes {includes}')
if limit >= 0 and offset >= 0:
_query = {'query': query, 'limit': limit, 'offset': offset}
else:
_query = {'query': query}
if includes is not None:
_query['inc'] = '+'.join(includes)
return _do_request(f'{_mb_host}/{entity_type}/?{urlencode(_query)}')
def _browse(entity_type, params, includes=None, limit=25, offset=0):
"""Browses entities of a type with the given parameters
:param str entity_type: Type of the entity (artist, release, recording...)
:param dict params: Dictionary with the parameters to do the search
:param list includes: List of include parameters (recording, releases...)
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: The JSON response
:rtype: dict
:raises ValueError if params is not a dictionary
"""
if not isinstance(params, dict):
raise ValueError('Params must be a dictionary')
if includes is None:
_log.info(f'Browsing {entity_type} with parameters {params} at offset {offset} with limit of {limit}')
_query = urlencode({**params, 'limit': limit, 'offset': offset})
else:
_log.info(f'Browsing {entity_type} with parameters {params} including {includes} at offset {offset} with limit of {limit}')
_query = urlencode({**params, 'inc': '+'.join(includes), 'limit': limit, 'offset': offset})
return _do_request(f'{_mb_host}/{entity_type}?{_query}')
def _ca(entity_type, mbid):
"""Gets the url of the cover art of a release or release-group
:param str entity_type: Type of the entity, could be release or release-group
:param str mbid: MBID of the entity of whom is the cover art
:return: The url of the cover art
"""
_log.info(f'Obtaining the cover art of the entity with type {entity_type} and mbid {mbid}')
_url = f'{_ca_host}/{entity_type}/{mbid}'
return _do_request(_url, host=_ca_host)
@cache
def get_artist_by_mbid(mbid, includes=None):
"""Get an artist by its mbid
:param str mbid: MBID of the artist
:param list includes: List of include parameters
:return: dictionary with the response
"""
return _get('artist', mbid, includes)
@cache
def get_release_group_by_mbid(mbid, includes=None):
"""Get a release group by its mbid
:param str mbid: MBID of the release group
:param list includes: List of include parameters
:return: dictionary with the response
"""
return _get('release-group', mbid, includes)
@cache
def get_release_by_mbid(mbid, includes=None):
"""Get a release by its mbid
:param str mbid: MBID of the release
:param list includes: List of include parameters
:return: dictionary with the response
"""
return _get('release', mbid, includes)
@cache
def get_recording_by_mbid(mbid, includes=None):
"""Get a recording by its mbid
:param str mbid: MBID of the recording
:param list includes: List of include parameters
:return: dictionary with the response
"""
return _get('recording', mbid, includes)
@cache
def search_artist(query, includes=None, limit=25, offset=0):
"""Search an artist by a query string
:param str query: Query string
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _search('artist', query, includes, limit, offset)
@cache
def search_release(query, includes=None, limit=25, offset=0):
"""Search a release by a query string
:param str query: Query string
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _search('release', query, includes, limit, offset)
@cache
def search_release_group(query, includes=None, limit=25, offset=0):
"""Search a release group by a query string
:param str query: Query string
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _search('release-group', query, includes, limit, offset)
@cache
def search_recording(query, includes=None, limit=25, offset=0):
"""Search a recording by a query string
:param str query: Query string
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _search('recording', query, includes,limit, offset)
@cache
def browse_artists(params, includes=None, limit=25, offset=0):
"""Browse an artist given some params
:param dict params: Parameters to do a search
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _browse('artist', params, includes, limit, offset)
@cache
def browse_recordings(params, includes=None, limit=25, offset=0):
"""Browse through recordings given some params
:param dict params: Parameters to do a search
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _browse('recording', params, includes, limit, offset)
@cache
def browse_releases(params, includes=None, limit=25, offset=0):
"""Browse through releases given some params
:param dict params: Parameters to do a search
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _browse('release', params, includes, limit, offset)
@cache
def browse_release_groups(params, includes=None, limit=25, offset=0):
"""Browse through release groups given some params
:param dict params: Parameters to do a search
:param list includes: List of include parameters
:param int limit: Limit of the search, defaults to 25 and has a max of 100
:param int offset: Offset of the search for paging purposes
:return: dictionary with the response
"""
return _browse('release-group', params, includes, limit, offset)
@cache
def get_release_cover_art(mbid):
"""Gets the url of the cover art of a release
:param str mbid: MBID of the release of whom is the cover art
:return: dictionary with the response
"""
return _ca('release', mbid)
@cache
def get_release_group_cover_art(mbid):
"""Gets the url of the cover art of a release group
:param str mbid: MBID of the release group of whom is the cover art
:return: dictionary with the response
"""
return _ca('release-group', mbid)