diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index d09f3a7d2..18d6a89c4 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -411,7 +411,7 @@
Settings
-
+
+
+
@@ -2456,7 +2473,7 @@ Settings
if ($("#torrent_downloader_qbittorrent").is(":checked"))
{
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
- }
+ }
if ($("#torrent_downloader_deluge").is(":checked"))
{
$("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html
index 260c0a1a3..e7becc618 100644
--- a/data/interfaces/default/index.html
+++ b/data/interfaces/default/index.html
@@ -36,7 +36,7 @@
"aTargets": [0],
"mData":"ArtistID",
"mRender": function ( data, type, full ) {
- return '![]()
';
+ return '![]()
';
}
},
{
diff --git a/headphones/cache.py b/headphones/cache.py
index 0e9466777..7b20862b8 100644
--- a/headphones/cache.py
+++ b/headphones/cache.py
@@ -16,10 +16,15 @@
import os
import headphones
-from headphones import db, helpers, logger, lastfm, request, mb
+from headphones import db, helpers, logger, lastfm, request, mb, os
+from fanart.music import Artist
-LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
+os.environ.setdefault('FANART_APIKEY', '1f081b32bcd780219f4e6d519f78e37e')
+if headphones.CONFIG.LASTFM_PERSONAL_KEY:
+ LASTFM_API_KEY = headphones.CONFIG.LASTFM_PERSONAL_KEY
+else:
+ LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
class Cache(object):
"""
@@ -106,22 +111,6 @@ def _is_current(self, filename=None, date=None):
else:
return False
- def _get_thumb_url(self, data):
-
- thumb_url = None
-
- try:
- images = data[self.id_type]['image']
- except KeyError:
- return None
-
- for image in images:
- if image['size'] == 'medium' and '#text' in image:
- thumb_url = image['#text']
- break
-
- return thumb_url
-
def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
@@ -139,7 +128,7 @@ def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
if self._exists('artwork') and self._is_current(filename=self.artwork_files[0]):
return self.artwork_files[0]
else:
- self._update_cache()
+ self._update_cache(ArtistID, AlbumID)
# If we failed to get artwork, either return the url or the older file
if self.artwork_errors and self.artwork_url:
return self.artwork_url
@@ -165,7 +154,7 @@ def get_thumb_from_cache(self, ArtistID=None, AlbumID=None):
if self._exists('thumb') and self._is_current(filename=self.thumb_files[0]):
return self.thumb_files[0]
else:
- self._update_cache()
+ self._update_cache(ArtistID, AlbumID)
# If we failed to get artwork, either return the url or the older file
if self.thumb_errors and self.thumb_url:
return self.thumb_url
@@ -195,7 +184,7 @@ def get_info_from_cache(self, ArtistID=None, AlbumID=None):
if not db_info or not db_info['LastUpdated'] or not self._is_current(
date=db_info['LastUpdated']):
- self._update_cache()
+ self._update_cache(ArtistID, AlbumID)
info_dict = {'Summary': self.info_summary, 'Content': self.info_content}
return info_dict
@@ -211,39 +200,62 @@ def get_image_links(self, ArtistID=None, AlbumID=None):
if ArtistID:
self.id_type = 'artist'
- data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
+ data = Artist.get(id=ArtistID)
+ logger.debug('Fanart.tv ArtistID: %s', ArtistID)
if not data:
+ logger.debug('Fanart.tv ArtistID not found!')
return
try:
- image_url = data['artist']['image'][-1]['#text']
+ for thumbs in data.thumbs[0:1]:
+ url = str(thumbs.url)
+ thumb_url = url.replace('fanart/', 'preview/')
+ image_url = thumb_url
+ logger.debug('Fanart.tv artist url: %s', image_url)
+ logger.debug('Fanart.tv artist thumb url: %s', thumb_url)
except (KeyError, IndexError):
- logger.debug('No artist image found')
+ logger.debug('Fanart.tv: No artist image found')
image_url = None
+ thumb_url = None
- thumb_url = self._get_thumb_url(data)
if not thumb_url:
- logger.debug('No artist thumbnail image found')
+ logger.debug('Fanart.tv: No artist thumbnail image found')
+
+ if not image_url:
+ logger.debug('Fanart.tv: No artist image found')
else:
self.id_type = 'album'
- data = lastfm.request_lastfm("album.getinfo", mbid=AlbumID, api_key=LASTFM_API_KEY)
+ data = Artist.get(id="ArtistID")
+ logger.debug('Fanart.tv AlbumID: %s', AlbumID)
+ for x in data.albums:
+ if x.mbid == AlbumID:
+ album_url = str(x.covers[0])
if not data:
+ logger.debug('Fanart.tv: Album not found')
return
try:
- image_url = data['album']['image'][-1]['#text']
+ for x in data.albums:
+ if x.mbid == AlbumID:
+ album_url = str(x.covers[0])
+ thumb_url = album_url.replace('fanart/', 'preview/')
+ image_url = thumb_url
+ logger.debug('Fanart.tv album url: %s', image_url)
+ logger.debug('Fanart.tv album thumb url: %s', thumb_url)
except (KeyError, IndexError):
- logger.debug('No album image found on last.fm')
+ logger.debug('Fanart.tv: No album image found')
image_url = None
-
- thumb_url = self._get_thumb_url(data)
+ thumb_url = None
if not thumb_url:
- logger.debug('No album thumbnail image found on last.fm')
+ logger.debug('Fanart.tv no album thumbnail image found on fanart.tv')
+
+ if not image_url:
+ logger.debug('Fanart.tv no album image found on fanart.tv')
return {'artwork': image_url, 'thumbnail': thumb_url}
@@ -277,7 +289,7 @@ def remove_from_cache(self, ArtistID=None, AlbumID=None):
except Exception:
logger.warn('Error deleting file from the cache: %s', thumb_file)
- def _update_cache(self):
+ def _update_cache(self, ArtistID, AlbumID):
"""
Since we call the same url for both info and artwork, we'll update both at the same time
"""
@@ -288,6 +300,28 @@ def _update_cache(self):
# Exception is when adding albums manually, then we should use release id
if self.id_type == 'artist':
+ data = Artist.get(id=self.id)
+
+ logger.debug('Fanart.tv ArtistID is: %s', self.id)
+
+ try:
+ for thumbs in data.thumbs[0:1]:
+ url = str(thumbs.url)
+ thumb_url = url.replace('fanart/', 'preview/')
+ image_url = thumb_url
+ logger.debug('Fanart.tv artist url: %s', image_url)
+ logger.debug('Fanart.tv artist thumb url: %s', thumb_url)
+ except KeyError:
+ logger.debug('Fanart.tv: No artist image found')
+ image_url = None
+ thumb_url = None
+
+ if not thumb_url:
+ logger.debug('Fanart.tv: No artist thumbnail image found')
+
+ if not image_url:
+ logger.debug('Fanart.tv: No artist image found')
+
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
# Try with name if not found
@@ -311,17 +345,38 @@ def _update_cache(self):
except KeyError:
logger.debug('No artist bio found')
self.info_content = None
+
+ else:
+
+ # get ArtistID from AlbumID lookup - ArtistID not passed into this function otherwise
+ myDB = db.DBConnection()
+ ArtistID = myDB.action('SELECT ArtistID FROM albums WHERE ReleaseID=?', [self.id]).fetchone()[0]
+
+ #ALBUM SECTION
+ logger.debug('Fanart.tv AlbumID: %s', AlbumID)
+ logger.debug('Fanart.tv ArtistID: %s', ArtistID)
+
+ data = Artist.get(id=ArtistID)
+
try:
- image_url = data['artist']['image'][-1]['#text']
+ for x in data.albums:
+ if x.mbid == AlbumID:
+ album_url = str(x.covers[0])
+ thumb_url = album_url.replace('fanart/', 'preview/')
+ image_url = thumb_url
+ logger.debug('Fanart.tv album url: %s', image_url)
+ logger.debug('Fanart.tv album thumb url: %s', thumb_url)
except KeyError:
- logger.debug('No artist image found')
+ logger.debug('Fanart.tv: No album image link found')
image_url = None
+ thumb_url = None
- thumb_url = self._get_thumb_url(data)
if not thumb_url:
- logger.debug('No artist thumbnail image found')
+ logger.debug('Fanart.tv: No album thumbnail image found')
+
+ if not image_url:
+ logger.debug('Fanart.tv: No album image found')
- else:
dbalbum = myDB.action(
'SELECT ArtistName, AlbumTitle, ReleaseID, Type FROM albums WHERE AlbumID=?',
[self.id]).fetchone()
@@ -362,16 +417,6 @@ def _update_cache(self):
except KeyError:
logger.debug('No album infomation found')
self.info_content = None
- try:
- image_url = data['album']['image'][-1]['#text']
- except KeyError:
- logger.debug('No album image link found')
- image_url = None
-
- thumb_url = self._get_thumb_url(data)
-
- if not thumb_url:
- logger.debug('No album thumbnail image found')
# Save the content & summary to the database no matter what if we've
# opened up the url
@@ -478,7 +523,6 @@ def _update_cache(self):
self.thumb_errors = True
self.thumb_url = image_url
-
def getArtwork(ArtistID=None, AlbumID=None):
c = Cache()
artwork_path = c.get_artwork_from_cache(ArtistID, AlbumID)
@@ -492,7 +536,6 @@ def getArtwork(ArtistID=None, AlbumID=None):
artwork_file = os.path.basename(artwork_path)
return "cache/artwork/" + artwork_file
-
def getThumb(ArtistID=None, AlbumID=None):
c = Cache()
artwork_path = c.get_thumb_from_cache(ArtistID, AlbumID)
@@ -506,14 +549,12 @@ def getThumb(ArtistID=None, AlbumID=None):
thumbnail_file = os.path.basename(artwork_path)
return "cache/artwork/" + thumbnail_file
-
def getInfo(ArtistID=None, AlbumID=None):
c = Cache()
info_dict = c.get_info_from_cache(ArtistID, AlbumID)
return info_dict
-
def getImageLinks(ArtistID=None, AlbumID=None):
c = Cache()
image_links = c.get_image_links(ArtistID, AlbumID)
diff --git a/headphones/config.py b/headphones/config.py
index 7a8b93fc4..7d77386ba 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -314,7 +314,9 @@ def __repr__(self):
'XBMC_PASSWORD': (str, 'XBMC', ''),
'XBMC_UPDATE': (int, 'XBMC', 0),
'XBMC_USERNAME': (str, 'XBMC', ''),
- 'XLDPROFILE': (str, 'General', '')
+ 'XLDPROFILE': (str, 'General', ''),
+ 'MUSICBRAINZ_USERAGENT': (str, 'General', ''),
+ 'LASTFM_PERSONAL_KEY': (str, 'General', '')
}
diff --git a/headphones/lastfm.py b/headphones/lastfm.py
index db3e1aec5..8b7568056 100644
--- a/headphones/lastfm.py
+++ b/headphones/lastfm.py
@@ -28,7 +28,6 @@
# Required for API request limit
lastfm_lock = headphones.lock.TimedLock(REQUEST_LIMIT)
-
def request_lastfm(method, **kwargs):
"""
Call a Last.FM API method. Automatically sets the method and API key. Method
@@ -37,7 +36,12 @@ def request_lastfm(method, **kwargs):
By default, this method will request the JSON format, since it is more
lightweight than XML.
"""
-
+
+ if headphones.CONFIG.LASTFM_PERSONAL_KEY:
+ API_KEY = headphones.CONFIG.LASTFM_PERSONAL_KEY
+ else:
+ API_KEY = "395e6ec6bb557382fc41fde867bce66f"
+
# Prepare request
kwargs["method"] = method
kwargs.setdefault("api_key", API_KEY)
@@ -45,6 +49,7 @@ def request_lastfm(method, **kwargs):
# Send request
logger.debug("Calling Last.FM method: %s", method)
+ logger.debug("Last.FM API Key is: %s" % API_KEY)
logger.debug("Last.FM call parameters: %s", kwargs)
data = request.request_json(ENTRY_POINT, timeout=TIMEOUT, params=kwargs, lock=lastfm_lock)
diff --git a/headphones/searcher.py b/headphones/searcher.py
index 1e51dc58e..d93a5cc14 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -436,7 +436,6 @@ def sort_search_results(resultlist, album, new, albumlength):
resultlist = temp_list
- # if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE and result[3] != 'Orpheus.network':
if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE:
try:
@@ -484,15 +483,16 @@ def sort_search_results(resultlist, album, new, albumlength):
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
- # keep number of seeders order for Orpheus.network
- # if result[3] == 'Orpheus.network':
- # finallist = resultlist
-
if not len(finallist):
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
album['AlbumTitle'])
return None
+ if result[3]:
+ if (result[3] == 'Orpheus.network') or (result[3] == 'Redacted'):
+ logger.info('Keeping torrents ordered by seeders for %s' % result[3])
+ finallist = resultlist
+
return finallist
@@ -1207,6 +1207,37 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
year = get_year_from_release_date(reldate)
+ # Specify release types to filter by - used by Orpheus and Redacted
+ # Could be added to any Gazelle-based music tracker
+ album_type = ""
+ if album['Type'] == 'Album':
+ album_type = [gazellerelease_type.ALBUM]
+ if album['Type'] == 'Soundtrack':
+ album_type = [gazellerelease_type.SOUNDTRACK]
+ if album['Type'] == 'EP':
+ album_type = [gazellerelease_type.EP]
+ # No musicbrainz match for this type
+ # if album['Type'] == 'Anthology':
+ # album_type = [gazellerelease_type.ANTHOLOGY]
+ if album['Type'] == 'Compilation':
+ album_type = [gazellerelease_type.COMPILATION]
+ if album['Type'] == 'DJ-mix':
+ album_type = [gazellerelease_type.DJ_MIX]
+ if album['Type'] == 'Single':
+ album_type = [gazellerelease_type.SINGLE]
+ if album['Type'] == 'Live':
+ album_type = [gazellerelease_type.LIVE_ALBUM]
+ if album['Type'] == 'Remix':
+ album_type = [gazellerelease_type.REMIX]
+ if album['Type'] == 'Bootleg':
+ album_type = [gazellerelease_type.BOOTLEG]
+ if album['Type'] == 'Interview':
+ album_type = [gazellerelease_type.INTERVIEW]
+ if album['Type'] == 'Mixtape/Street':
+ album_type = [gazellerelease_type.MIXTAPE]
+ if album['Type'] == 'Other':
+ album_type = [gazellerelease_type.UNKNOWN]
+
# MERGE THIS WITH THE TERM CLEANUP FROM searchNZB
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': ' ',
'*': ''}
@@ -1514,37 +1545,6 @@ def set_proxy(proxy_url):
logger.info(u"Searching %s..." % provider)
all_torrents = []
- album_type = ""
-
- # Specify release types to filter by
- if album['Type'] == 'Album':
- album_type = [gazellerelease_type.ALBUM]
- if album['Type'] == 'Soundtrack':
- album_type = [gazellerelease_type.SOUNDTRACK]
- if album['Type'] == 'EP':
- album_type = [gazellerelease_type.EP]
- # No musicbrainz match for this type
- # if album['Type'] == 'Anthology':
- # album_type = [gazellerelease_type.ANTHOLOGY]
- if album['Type'] == 'Compilation':
- album_type = [gazellerelease_type.COMPILATION]
- if album['Type'] == 'DJ-mix':
- album_type = [gazellerelease_type.DJ_MIX]
- if album['Type'] == 'Single':
- album_type = [gazellerelease_type.SINGLE]
- if album['Type'] == 'Live':
- album_type = [gazellerelease_type.LIVE_ALBUM]
- if album['Type'] == 'Remix':
- album_type = [gazellerelease_type.REMIX]
- if album['Type'] == 'Bootleg':
- album_type = [gazellerelease_type.BOOTLEG]
- if album['Type'] == 'Interview':
- album_type = [gazellerelease_type.INTERVIEW]
- if album['Type'] == 'Mixtape/Street':
- album_type = [gazellerelease_type.MIXTAPE]
- if album['Type'] == 'Other':
- album_type = [gazellerelease_type.UNKNOWN]
-
for search_format in search_formats:
if usersearchterm:
all_torrents.extend(
@@ -1644,16 +1644,18 @@ def set_proxy(proxy_url):
if redobj and redobj.logged_in():
logger.info(u"Searching %s..." % provider)
all_torrents = []
+
for search_format in search_formats:
if usersearchterm:
all_torrents.extend(
redobj.search_torrents(searchstr=usersearchterm, format=search_format,
- encoding=bitrate_string)['results'])
+ encoding=bitrate_string, releasetype=album_type)['results'])
else:
all_torrents.extend(redobj.search_torrents(artistname=semi_clean_artist_term,
groupname=semi_clean_album_term,
format=search_format,
- encoding=bitrate_string)['results'])
+ encoding=bitrate_string,
+ releasetype=album_type)['results'])
# filter on format, size, and num seeders
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
diff --git a/headphones/webserve.py b/headphones/webserve.py
index 9e2f1979f..4758faa3a 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -1415,7 +1415,9 @@ def config(self):
"join_enabled": checked(headphones.CONFIG.JOIN_ENABLED),
"join_onsnatch": checked(headphones.CONFIG.JOIN_ONSNATCH),
"join_apikey": headphones.CONFIG.JOIN_APIKEY,
- "join_deviceid": headphones.CONFIG.JOIN_DEVICEID
+ "join_deviceid": headphones.CONFIG.JOIN_DEVICEID,
+ "musicbrainz_useragent": headphones.CONFIG.MUSICBRAINZ_USERAGENT,
+ "lastfm_personal_key": headphones.CONFIG.LASTFM_PERSONAL_KEY
}
for k, v in config.iteritems():
diff --git a/lib/fanart/__init__.py b/lib/fanart/__init__.py
new file mode 100644
index 000000000..01d686b53
--- /dev/null
+++ b/lib/fanart/__init__.py
@@ -0,0 +1,118 @@
+__author__ = 'Andrea De Marco <24erre@gmail.com>'
+__maintainer__ = 'Pol Canelles '
+__version__ = '2.0.0'
+__classifiers__ = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Software Development :: Libraries',
+]
+__copyright__ = "2012, %s " % __author__
+__license__ = """
+ Copyright %s.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+""" % __copyright__
+
+__docformat__ = 'restructuredtext en'
+
+__doc__ = """
+:abstract: Python interface to fanart.tv API (v3)
+:version: %s
+:author: %s
+:contact: http://z4r.github.com/
+:date: 2012-04-04
+:copyright: %s
+""" % (__version__, __author__, __license__)
+
+
+def values(obj):
+ return [v for k, v in obj.__dict__.items() if not k.startswith('_')]
+
+
+BASEURL = 'http://webservice.fanart.tv/v3'
+
+
+class FORMAT(object):
+ JSON = 'JSON'
+ XML = 'XML'
+ PHP = 'PHP'
+
+
+class WS(object):
+ MUSIC = 'music'
+ MOVIE = 'movies'
+ TV = 'tv'
+
+
+class TYPE(object):
+ ALL = 'all'
+
+ class TV(object):
+ ART = 'clearart'
+ LOGO = 'clearlogo'
+ CHARACTER = 'characterart'
+ THUMB = 'tvthumb'
+ SEASONTHUMB = 'seasonthumb'
+ SEASONBANNER = 'seasonbanner'
+ SEASONPOSTER = 'seasonposter'
+ BACKGROUND = 'showbackground'
+ HDLOGO = 'hdtvlogo'
+ HDART = 'hdclearart'
+ POSTER = 'tvposter'
+ BANNER = 'tvbanner'
+
+ class MUSIC(object):
+ DISC = 'cdart'
+ LOGO = 'musiclogo'
+ BACKGROUND = 'artistbackground'
+ COVER = 'albumcover'
+ THUMB = 'artistthumb'
+
+ class MOVIE(object):
+ ART = 'movieart'
+ LOGO = 'movielogo'
+ DISC = 'moviedisc'
+ POSTER = 'movieposter'
+ BACKGROUND = 'moviebackground'
+ HDLOGO = 'hdmovielogo'
+ HDART = 'hdmovieclearart'
+ BANNER = 'moviebanner'
+ THUMB = 'moviethumb'
+
+
+class SORT(object):
+ POPULAR = 1
+ NEWEST = 2
+ OLDEST = 3
+
+
+class LIMIT(object):
+ ONE = 1
+ ALL = 2
+
+
+FORMAT_LIST = values(FORMAT)
+WS_LIST = values(WS)
+TYPE_LIST = values(TYPE.MUSIC) + values(TYPE.TV) + values(TYPE.MOVIE) + [TYPE.ALL]
+MUSIC_TYPE_LIST = values(TYPE.MUSIC) + [TYPE.ALL]
+TV_TYPE_LIST = values(TYPE.TV) + [TYPE.ALL]
+MOVIE_TYPE_LIST = values(TYPE.MOVIE) + [TYPE.ALL]
+SORT_LIST = values(SORT)
+LIMIT_LIST = values(LIMIT)
diff --git a/lib/fanart/core.py b/lib/fanart/core.py
new file mode 100644
index 000000000..8e01bab79
--- /dev/null
+++ b/lib/fanart/core.py
@@ -0,0 +1,49 @@
+import requests
+import fanart
+from fanart.errors import RequestFanartError, ResponseFanartError
+
+
+class Request(object):
+ def __init__(self, apikey, id, ws, type=None, sort=None, limit=None):
+ '''
+ .. warning:: Since the migration to fanart.tv's api v3, we cannot use
+ the kwargs `type/sort/limit` as we did before, so for now this
+ kwargs will be ignored.
+ '''
+ self._apikey = apikey
+ self._id = id
+ self._ws = ws
+ self._type = type or fanart.TYPE.ALL
+ self._sort = sort or fanart.SORT.POPULAR
+ self._limit = limit or fanart.LIMIT.ALL
+ self.validate()
+ self._response = None
+
+ def validate(self):
+ for attribute_name in ('ws', 'type', 'sort', 'limit'):
+ attribute = getattr(self, '_' + attribute_name)
+ choices = getattr(fanart, attribute_name.upper() + '_LIST')
+ if attribute not in choices:
+ raise RequestFanartError(
+ 'Not allowed {}: {} [{}]'.format(
+ attribute_name, attribute, ', '.join(choices)))
+
+ def __str__(self):
+ return '{base_url}/{ws}/{id}?api_key={apikey}'.format(
+ base_url=fanart.BASEURL,
+ ws=self._ws,
+ id=self._id,
+ apikey=self._apikey,
+ )
+
+ def response(self):
+ try:
+ response = requests.get(str(self))
+ rjson = response.json()
+ if not isinstance(rjson, dict):
+ raise Exception(response.text)
+ if 'error message' in rjson:
+ raise Exception(rjson['status'], rjson['error message'])
+ return rjson
+ except Exception as e:
+ raise ResponseFanartError(str(e))
diff --git a/lib/fanart/errors.py b/lib/fanart/errors.py
new file mode 100644
index 000000000..95a71e35e
--- /dev/null
+++ b/lib/fanart/errors.py
@@ -0,0 +1,15 @@
+class FanartError(Exception):
+ def __str__(self):
+ return ', '.join(map(str, self.args))
+
+ def __repr__(self):
+ name = self.__class__.__name__
+ return '%s%r' % (name, self.args)
+
+
+class ResponseFanartError(FanartError):
+ pass
+
+
+class RequestFanartError(FanartError):
+ pass
diff --git a/lib/fanart/immutable.py b/lib/fanart/immutable.py
new file mode 100644
index 000000000..d51ce1826
--- /dev/null
+++ b/lib/fanart/immutable.py
@@ -0,0 +1,46 @@
+class Immutable(object):
+ _mutable = False
+
+ def __setattr__(self, name, value):
+ if self._mutable or name == '_mutable':
+ super(Immutable, self).__setattr__(name, value)
+ else:
+ raise TypeError("Can't modify immutable instance")
+
+ def __delattr__(self, name):
+ if self._mutable:
+ super(Immutable, self).__delattr__(name)
+ else:
+ raise TypeError("Can't modify immutable instance")
+
+ def __eq__(self, other):
+ return hash(self) == hash(other)
+
+ def __hash__(self):
+ return hash(repr(self))
+
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ ', '.join(['{0}={1}'.format(k, repr(v)) for k, v in self])
+ )
+
+ def __iter__(self):
+ l = list(self.__dict__.keys())
+ l.sort()
+ for k in l:
+ if not k.startswith('_'):
+ yield k, getattr(self, k)
+
+ @staticmethod
+ def mutablemethod(f):
+ def func(self, *args, **kwargs):
+ if isinstance(self, Immutable):
+ old_mutable = self._mutable
+ self._mutable = True
+ res = f(self, *args, **kwargs)
+ self._mutable = old_mutable
+ else:
+ res = f(self, *args, **kwargs)
+ return res
+ return func
diff --git a/lib/fanart/items.py b/lib/fanart/items.py
new file mode 100644
index 000000000..77bd85306
--- /dev/null
+++ b/lib/fanart/items.py
@@ -0,0 +1,70 @@
+import json
+import os
+import requests
+from fanart.core import Request
+from fanart.immutable import Immutable
+
+
+class LeafItem(Immutable):
+ KEY = NotImplemented
+
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes):
+ self.id = int(id)
+ self.url = url
+ self.likes = int(likes)
+ self._content = None
+
+ @classmethod
+ def from_dict(cls, resource):
+ return cls(**dict([(str(k), v) for k, v in resource.items()]))
+
+ @classmethod
+ def extract(cls, resource):
+ return [cls.from_dict(i) for i in resource.get(cls.KEY, {})]
+
+ @Immutable.mutablemethod
+ def content(self):
+ if not self._content:
+ self._content = requests.get(self.url).content
+ return self._content
+
+ def __str__(self):
+ return self.url
+
+
+class ResourceItem(Immutable):
+ WS = NotImplemented
+ request_cls = Request
+
+ @classmethod
+ def from_dict(cls, map):
+ raise NotImplementedError
+
+ @classmethod
+ def get(cls, id):
+ map = cls.request_cls(
+ apikey=os.environ.get('FANART_APIKEY'),
+ id=id,
+ ws=cls.WS
+ ).response()
+ return cls.from_dict(map)
+
+ def json(self, **kw):
+ return json.dumps(
+ self,
+ default=lambda o: dict(
+ [(k, v) for k, v in o.__dict__.items()
+ if not k.startswith('_')]),
+ **kw
+ )
+
+
+class CollectableItem(Immutable):
+ @classmethod
+ def from_dict(cls, key, map):
+ raise NotImplementedError
+
+ @classmethod
+ def collection_from_dict(cls, map):
+ return [cls.from_dict(k, v) for k, v in map.items()]
diff --git a/lib/fanart/movie.py b/lib/fanart/movie.py
new file mode 100644
index 000000000..1ff2c1238
--- /dev/null
+++ b/lib/fanart/movie.py
@@ -0,0 +1,103 @@
+import fanart
+from fanart.items import LeafItem, Immutable, ResourceItem
+__all__ = (
+ 'ArtItem',
+ 'DiscItem',
+ 'LogoItem',
+ 'PosterItem',
+ 'BackgroundItem',
+ 'HdLogoItem',
+ 'HdArtItem',
+ 'BannerItem',
+ 'ThumbItem',
+ 'Movie',
+)
+
+
+class MovieItem(LeafItem):
+
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes, lang):
+ super(MovieItem, self).__init__(id, url, likes)
+ self.lang = lang
+
+
+class DiscItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.DISC
+
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes, lang, disc, disc_type):
+ super(DiscItem, self).__init__(id, url, likes, lang)
+ self.disc = int(disc)
+ self.disc_type = disc_type
+
+
+class ArtItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.ART
+
+
+class LogoItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.LOGO
+
+
+class PosterItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.POSTER
+
+
+class BackgroundItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.BACKGROUND
+
+
+class HdLogoItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.HDLOGO
+
+
+class HdArtItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.HDART
+
+
+class BannerItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.BANNER
+
+
+class ThumbItem(MovieItem):
+ KEY = fanart.TYPE.MOVIE.THUMB
+
+
+class Movie(ResourceItem):
+ WS = fanart.WS.MOVIE
+
+ @Immutable.mutablemethod
+ def __init__(self, name, imdbid, tmdbid, arts, logos, discs, posters,
+ backgrounds, hdlogos, hdarts, banners, thumbs):
+ self.name = name
+ self.imdbid = imdbid
+ self.tmdbid = tmdbid
+ self.arts = arts
+ self.posters = posters
+ self.logos = logos
+ self.discs = discs
+ self.backgrounds = backgrounds
+ self.hdlogos = hdlogos
+ self.hdarts = hdarts
+ self.banners = banners
+ self.thumbs = thumbs
+
+ @classmethod
+ def from_dict(cls, resource):
+ minimal_keys = {'name', 'imdb_id', 'tmdb_id'}
+ assert all(k in resource for k in minimal_keys), 'Bad Format Map'
+ return cls(
+ name=resource['name'],
+ imdbid=resource['imdb_id'],
+ tmdbid=resource['tmdb_id'],
+ arts=ArtItem.extract(resource),
+ logos=LogoItem.extract(resource),
+ discs=DiscItem.extract(resource),
+ posters=PosterItem.extract(resource),
+ backgrounds=BackgroundItem.extract(resource),
+ hdlogos=HdLogoItem.extract(resource),
+ hdarts=HdArtItem.extract(resource),
+ banners=BannerItem.extract(resource),
+ thumbs=ThumbItem.extract(resource),
+ )
diff --git a/lib/fanart/music.py b/lib/fanart/music.py
new file mode 100644
index 000000000..61e23d231
--- /dev/null
+++ b/lib/fanart/music.py
@@ -0,0 +1,80 @@
+from fanart.items import Immutable, LeafItem, ResourceItem, CollectableItem
+import fanart
+__all__ = (
+ 'BackgroundItem',
+ 'CoverItem',
+ 'LogoItem',
+ 'ThumbItem',
+ 'DiscItem',
+ 'Artist',
+ 'Album',
+)
+
+
+class BackgroundItem(LeafItem):
+ KEY = fanart.TYPE.MUSIC.BACKGROUND
+
+
+class CoverItem(LeafItem):
+ KEY = fanart.TYPE.MUSIC.COVER
+
+
+class LogoItem(LeafItem):
+ KEY = fanart.TYPE.MUSIC.LOGO
+
+
+class ThumbItem(LeafItem):
+ KEY = fanart.TYPE.MUSIC.THUMB
+
+
+class DiscItem(LeafItem):
+ KEY = fanart.TYPE.MUSIC.DISC
+
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes, disc, size):
+ super(DiscItem, self).__init__(id, url, likes)
+ self.disc = int(disc)
+ self.size = int(size)
+
+
+class Artist(ResourceItem):
+ WS = fanart.WS.MUSIC
+
+ @Immutable.mutablemethod
+ def __init__(self, name, mbid, albums, backgrounds, logos, thumbs):
+ self.name = name
+ self.mbid = mbid
+ self.albums = albums
+ self.backgrounds = backgrounds
+ self.logos = logos
+ self.thumbs = thumbs
+
+ @classmethod
+ def from_dict(cls, resource):
+ minimal_keys = {'name', 'mbid_id'}
+ assert all(k in resource for k in minimal_keys), 'Bad Format Map'
+ return cls(
+ name=resource['name'],
+ mbid=resource['mbid_id'],
+ albums=Album.collection_from_dict(resource.get('albums', {})),
+ backgrounds=BackgroundItem.extract(resource),
+ thumbs=ThumbItem.extract(resource),
+ logos=LogoItem.extract(resource),
+ )
+
+
+class Album(CollectableItem):
+
+ @Immutable.mutablemethod
+ def __init__(self, mbid, covers, arts):
+ self.mbid = mbid
+ self.covers = covers
+ self.arts = arts
+
+ @classmethod
+ def from_dict(cls, key, resource):
+ return cls(
+ mbid=key,
+ covers=CoverItem.extract(resource),
+ arts=DiscItem.extract(resource),
+ )
diff --git a/lib/fanart/tests/__init__.py b/lib/fanart/tests/__init__.py
new file mode 100644
index 000000000..957cbe388
--- /dev/null
+++ b/lib/fanart/tests/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+LOCALDIR = os.path.dirname(__file__)
diff --git a/lib/fanart/tests/json/wilfred.json b/lib/fanart/tests/json/wilfred.json
new file mode 100644
index 000000000..07687e090
--- /dev/null
+++ b/lib/fanart/tests/json/wilfred.json
@@ -0,0 +1,327 @@
+{
+ "name": "Wilfred (US)",
+ "tvdbid": "239761",
+ "backgrounds": [
+ {
+ "id": 19965,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-5034dbd49115e.jpg",
+ "likes": 0,
+ "lang": "",
+ "season": 0
+ },
+ {
+ "id": 23166,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92db6973.jpg",
+ "likes": 0,
+ "lang": "",
+ "season": 0
+ },
+ {
+ "id": 23167,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92dbb46b.jpg",
+ "likes": 0,
+ "lang": "",
+ "season": 0
+ },
+ {
+ "id": 23168,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92dbb9d1.jpg",
+ "likes": 0,
+ "lang": "",
+ "season": 0
+ },
+ {
+ "id": 36386,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-52d53d75d4782.jpg",
+ "likes": 0,
+ "lang": "",
+ "season": 0
+ }
+ ],
+ "characters": [],
+ "arts": [
+ {
+ "id": 11987,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearart/wilfred-us-4e05f10e87711.png",
+ "likes": 2,
+ "lang": "en"
+ },
+ {
+ "id": 12470,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearart/wilfred-us-4e2f151d5ed62.png",
+ "likes": 1,
+ "lang": "en"
+ }
+ ],
+ "logos": [
+ {
+ "id": 11977,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-4e04b6495dfd3.png",
+ "likes": 2,
+ "lang": "en"
+ },
+ {
+ "id": 28249,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-517ac36e39f67.png",
+ "likes": 1,
+ "lang": "en"
+ },
+ {
+ "id": 31817,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-51f557082cfde.png",
+ "likes": 0,
+ "lang": "en"
+ }
+ ],
+ "seasons": [
+ {
+ "id": 33752,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-52403782bab55.jpg",
+ "likes": 1,
+ "lang": "he",
+ "season": 1
+ },
+ {
+ "id": 33753,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-5240379335232.jpg",
+ "likes": 1,
+ "lang": "he",
+ "season": 2
+ },
+ {
+ "id": 33754,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-524037bc83c7d.jpg",
+ "likes": 1,
+ "lang": "he",
+ "season": 3
+ },
+ {
+ "id": 19586,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb0a8e60f9.jpg",
+ "likes": 0,
+ "lang": "en",
+ "season": 1
+ },
+ {
+ "id": 19587,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb0b4bf229.jpg",
+ "likes": 0,
+ "lang": "en",
+ "season": 2
+ },
+ {
+ "id": 19588,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb144e6a46.jpg",
+ "likes": 0,
+ "lang": "en",
+ "season": 0
+ },
+ {
+ "id": 30309,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-51c953105ef77.jpg",
+ "likes": 0,
+ "lang": "en",
+ "season": 3
+ }
+ ],
+ "thumbs": [
+ {
+ "id": 19596,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvthumb/wilfred-us-501cf526174fe.jpg",
+ "likes": 1,
+ "lang": "en"
+ },
+ {
+ "id": 30060,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvthumb/wilfred-us-51bfb4a105904.jpg",
+ "likes": 1,
+ "lang": "en"
+ }
+ ],
+ "hdlogos": [
+ {
+ "id": 21101,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-505f373be58e6.png",
+ "likes": 2,
+ "lang": "en"
+ },
+ {
+ "id": 28248,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-517ac360def17.png",
+ "likes": 2,
+ "lang": "en"
+ },
+ {
+ "id": 33750,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-52402df7ed945.png",
+ "likes": 1,
+ "lang": "he"
+ },
+ {
+ "id": 42207,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-53d12425b2d8c.png",
+ "likes": 1,
+ "lang": "ru"
+ },
+ {
+ "id": 31816,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-51f556fb4abd3.png",
+ "likes": 0,
+ "lang": "en"
+ }
+ ],
+ "hdarts": [
+ {
+ "id": 21112,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdclearart/wilfred-us-505f94ed0ba13.png",
+ "likes": 1,
+ "lang": "en"
+ },
+ {
+ "id": 33751,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdclearart/wilfred-us-52403264aa3ec.png",
+ "likes": 1,
+ "lang": "he"
+ }
+ ],
+ "posters": [
+ {
+ "id": 34584,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvposter/wilfred-us-525d893230d7c.jpg",
+ "likes": 1,
+ "lang": "he"
+ }
+ ],
+ "banners": [
+ {
+ "id": 33755,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52403a7185070.jpg",
+ "likes": 1,
+ "lang": "he"
+ },
+ {
+ "id": 57138,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5607140bac1c6.jpg",
+ "likes": 1,
+ "lang": "en"
+ },
+ {
+ "id": 34716,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5265193db51f7.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 36389,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578d57cb24.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 36390,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578de27a66.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41457,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da4cd2a21.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41458,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da6b594e5.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41685,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7db8014425.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41686,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7dbb922332.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 57139,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-56071512b14d6.jpg",
+ "likes": 0,
+ "lang": "en"
+ }
+ ],
+ "season_posters": [
+ {
+ "id": 34584,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvposter/wilfred-us-525d893230d7c.jpg",
+ "likes": 1,
+ "lang": "he"
+ }
+ ],
+ "season_banners": [
+ {
+ "id": 33755,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52403a7185070.jpg",
+ "likes": 1,
+ "lang": "he"
+ },
+ {
+ "id": 57138,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5607140bac1c6.jpg",
+ "likes": 1,
+ "lang": "en"
+ },
+ {
+ "id": 34716,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5265193db51f7.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 36389,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578d57cb24.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 36390,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578de27a66.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41457,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da4cd2a21.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41458,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da6b594e5.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41685,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7db8014425.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 41686,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7dbb922332.jpg",
+ "likes": 0,
+ "lang": "en"
+ },
+ {
+ "id": 57139,
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-56071512b14d6.jpg",
+ "likes": 0,
+ "lang": "en"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/fanart/tests/response/50x50.png b/lib/fanart/tests/response/50x50.png
new file mode 100644
index 000000000..0ba41614c
Binary files /dev/null and b/lib/fanart/tests/response/50x50.png differ
diff --git a/lib/fanart/tests/response/movie_thg.json b/lib/fanart/tests/response/movie_thg.json
new file mode 100644
index 000000000..e550eef8a
--- /dev/null
+++ b/lib/fanart/tests/response/movie_thg.json
@@ -0,0 +1,643 @@
+{
+ "name": "The Hunger Games",
+ "tmdb_id": "70160",
+ "imdb_id": "tt1392170",
+ "hdmovielogo": [
+ {
+ "id": "57659",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528f5039e0952.png",
+ "lang": "en",
+ "likes": "11"
+ },
+ {
+ "id": "57687",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528f7e2ac443b.png",
+ "lang": "en",
+ "likes": "7"
+ },
+ {
+ "id": "57519",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528e559d3b417.png",
+ "lang": "fr",
+ "likes": "7"
+ },
+ {
+ "id": "57688",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528f7e3e86057.png",
+ "lang": "fr",
+ "likes": "4"
+ },
+ {
+ "id": "67088",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-5325b5e6b9d98.png",
+ "lang": "es",
+ "likes": "2"
+ },
+ {
+ "id": "57657",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528f48e712885.png",
+ "lang": "de",
+ "likes": "2"
+ },
+ {
+ "id": "57686",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-528f7e1420556.png",
+ "lang": "de",
+ "likes": "2"
+ },
+ {
+ "id": "153458",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-573f577d2629a.png",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "153530",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-573f87f11ff6f.png",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "153546",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-5740179f7f08a.png",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "58172",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-5296dd1494834.png",
+ "lang": "ru",
+ "likes": "0"
+ },
+ {
+ "id": "58173",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovielogo/the-hunger-games-5296dd22a7d2a.png",
+ "lang": "ru",
+ "likes": "0"
+ }
+ ],
+ "hdmovieclearart": [
+ {
+ "id": "58177",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5296dda1af66e.png",
+ "lang": "en",
+ "likes": "8"
+ },
+ {
+ "id": "57718",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-528fccd9afa08.png",
+ "lang": "fr",
+ "likes": "4"
+ },
+ {
+ "id": "138395",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-56c3bd2f426a2.png",
+ "lang": "es",
+ "likes": "3"
+ },
+ {
+ "id": "14104",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-50582453b1375.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "36754",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-51a76f9b35b68.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "26534",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-513643b6879c0.png",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "55612",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f46398443.png",
+ "lang": "de",
+ "likes": "0"
+ },
+ {
+ "id": "55613",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f46398b85.png",
+ "lang": "de",
+ "likes": "0"
+ },
+ {
+ "id": "55614",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f47ddc1aa.png",
+ "lang": "pl",
+ "likes": "0"
+ },
+ {
+ "id": "55615",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f47ddcb0d.png",
+ "lang": "pl",
+ "likes": "0"
+ },
+ {
+ "id": "55616",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f499b2efe.png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "58176",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5296dd8ad1105.png",
+ "lang": "ru",
+ "likes": "0"
+ },
+ {
+ "id": "55617",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5273f499b35c8.png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "26539",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/hdmovieclearart/the-hunger-games-5136534a62a4e.png",
+ "lang": "pl",
+ "likes": "0"
+ }
+ ],
+ "moviedisc": [
+ {
+ "id": "26728",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-5139a08c09956.png",
+ "lang": "en",
+ "likes": "7",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "85058",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-5431702449d26.png",
+ "lang": "fr",
+ "likes": "6",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "93048",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-54a48d1f50a3b.png",
+ "lang": "en",
+ "likes": "6",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "85057",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-54317005eb45e.png",
+ "lang": "en",
+ "likes": "5",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "142709",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-56e43b138db23.png",
+ "lang": "en",
+ "likes": "4",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "8431",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-501db4437623f.png",
+ "lang": "en",
+ "likes": "4",
+ "disc": "1",
+ "disc_type": "dvd"
+ },
+ {
+ "id": "93054",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-54a49bd5af0b9.png",
+ "lang": "en",
+ "likes": "3",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "122835",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-5607b3241fc8a.png",
+ "lang": "es",
+ "likes": "3",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "9787",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-502fd6d695a60.png",
+ "lang": "en",
+ "likes": "2",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "99861",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-54fc56a87f8e1.png",
+ "lang": "de",
+ "likes": "1",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "35016",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-519554a51d708.png",
+ "lang": "de",
+ "likes": "1",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "85059",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-5431704438eb6.png",
+ "lang": "de",
+ "likes": "0",
+ "disc": "1",
+ "disc_type": "bluray"
+ },
+ {
+ "id": "85060",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviedisc/the-hunger-games-54317064bf8fb.png",
+ "lang": "ru",
+ "likes": "0",
+ "disc": "1",
+ "disc_type": "bluray"
+ }
+ ],
+ "movieposter": [
+ {
+ "id": "52850",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-52497d189110b.jpg",
+ "lang": "en",
+ "likes": "7"
+ },
+ {
+ "id": "58174",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5296dd471d639.jpg",
+ "lang": "00",
+ "likes": "6"
+ },
+ {
+ "id": "99774",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-54fb39df92832.jpg",
+ "lang": "en",
+ "likes": "6"
+ },
+ {
+ "id": "93052",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-54a494cda1840.jpg",
+ "lang": "00",
+ "likes": "4"
+ },
+ {
+ "id": "218233",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5a7f8a078c90e.jpg",
+ "lang": "es",
+ "likes": "3"
+ },
+ {
+ "id": "218234",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5a7f8a16ec046.jpg",
+ "lang": "es",
+ "likes": "3"
+ },
+ {
+ "id": "93053",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-54a494cda2176.jpg",
+ "lang": "00",
+ "likes": "2"
+ },
+ {
+ "id": "74735",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-53b119c0100fc.jpg",
+ "lang": "fr",
+ "likes": "2"
+ },
+ {
+ "id": "68608",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5335a39219806.jpg",
+ "lang": "de",
+ "likes": "1"
+ },
+ {
+ "id": "142620",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-56e32926f255a.jpg",
+ "lang": "fr",
+ "likes": "1"
+ },
+ {
+ "id": "58175",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5296dd62a8d38.jpg",
+ "lang": "ru",
+ "likes": "1"
+ },
+ {
+ "id": "93051",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-54a494cda0922.jpg",
+ "lang": "00",
+ "likes": "1"
+ },
+ {
+ "id": "171203",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5831e4f72df33.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "171204",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5831e4f72eb10.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "171205",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5831e4f72f130.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "171206",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-5831e4f72f6b9.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "153550",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-574027c50b23a.jpg",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "153551",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-574027c50bac9.jpg",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "153552",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-574027c50c00d.jpg",
+ "lang": "pl",
+ "likes": "1"
+ },
+ {
+ "id": "142619",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-56e3291171ee2.jpg",
+ "lang": "fr",
+ "likes": "0"
+ },
+ {
+ "id": "62784",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-52e6a9036c84f.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "154795",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieposter/the-hunger-games-57500557a49a1.jpg",
+ "lang": "de",
+ "likes": "0"
+ }
+ ],
+ "movieart": [
+ {
+ "id": "1226",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieart/the-hunger-games-4f6dc995edb8f.png",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "1225",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movieart/the-hunger-games-4f6dc980b4514.png",
+ "lang": "en",
+ "likes": "2"
+ }
+ ],
+ "movielogo": [
+ {
+ "id": "8020",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movielogo/the-hunger-games-5018f873b5188.png",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "1230",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movielogo/the-hunger-games-4f6e0e63a9d29.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "1224",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/movielogo/the-hunger-games-4f6dc95a08de1.png",
+ "lang": "en",
+ "likes": "1"
+ }
+ ],
+ "moviethumb": [
+ {
+ "id": "63582",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-52f19dd4dc531.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "10687",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-503c88b32cf66.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "138365",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-56c387040943b.jpg",
+ "lang": "es",
+ "likes": "2"
+ },
+ {
+ "id": "138366",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-56c3870f3b889.jpg",
+ "lang": "es",
+ "likes": "2"
+ },
+ {
+ "id": "136921",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-56b4ecdb4cb66.jpg",
+ "lang": "fr",
+ "likes": "1"
+ },
+ {
+ "id": "53391",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-5251d9706cf40.jpg",
+ "lang": "de",
+ "likes": "0"
+ },
+ {
+ "id": "53392",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-5251d97d2bc96.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "136920",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviethumb/the-hunger-games-56b4ecdb4c45f.jpg",
+ "lang": "fr",
+ "likes": "0"
+ }
+ ],
+ "moviebackground": [
+ {
+ "id": "15919",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e12006159.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15921",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e206aa2ac.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15922",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e2869d774.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15925",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e30069b72.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15927",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e3c4979b7.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15930",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e5b3f039b.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15931",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e6369e812.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15936",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e8749e73a.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15937",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e9913bfeb.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "142658",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-56e3efb2adbd7.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "142659",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-56e3efb2af62f.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "14043",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5057c79ad3c56.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "14044",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5057c79ad5526.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "24822",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-51208e3788477.jpg",
+ "lang": "",
+ "likes": "2"
+ },
+ {
+ "id": "15911",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071de49311d1.jpg",
+ "lang": "",
+ "likes": "1"
+ },
+ {
+ "id": "15914",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071df619b835.jpg",
+ "lang": "",
+ "likes": "1"
+ },
+ {
+ "id": "15917",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e01fee856.jpg",
+ "lang": "",
+ "likes": "1"
+ },
+ {
+ "id": "15918",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-5071e0adcc57a.jpg",
+ "lang": "",
+ "likes": "1"
+ },
+ {
+ "id": "24823",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebackground/the-hunger-games-51208f2b8b580.jpg",
+ "lang": "",
+ "likes": "1"
+ }
+ ],
+ "moviebanner": [
+ {
+ "id": "28528",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-514ec04ad47de.jpg",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "138364",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-56c386f7f149b.jpg",
+ "lang": "es",
+ "likes": "2"
+ },
+ {
+ "id": "136919",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-56b4ecc453ffb.jpg",
+ "lang": "fr",
+ "likes": "1"
+ },
+ {
+ "id": "122241",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-56008d6a4885e.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "53393",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-5251da676190e.jpg",
+ "lang": "de",
+ "likes": "0"
+ },
+ {
+ "id": "136918",
+ "url": "https://assets.fanart.tv/fanart/movies/70160/moviebanner/the-hunger-games-56b4ecc453971.jpg",
+ "lang": "fr",
+ "likes": "0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/fanart/tests/response/music_a7f.json b/lib/fanart/tests/response/music_a7f.json
new file mode 100644
index 000000000..a3dc70f8f
--- /dev/null
+++ b/lib/fanart/tests/response/music_a7f.json
@@ -0,0 +1,648 @@
+{
+ "name": "Avenged Sevenfold",
+ "mbid_id": "24e1b53c-3085-4581-8472-0b0088d2508c",
+ "albums": {
+ "fe4373ed-5e89-46b3-b4c0-31433ce217df": {
+ "albumcover": [
+ {
+ "id": "128441",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/nightmare-53b398b4d032e.jpg",
+ "likes": "4"
+ },
+ {
+ "id": "128499",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/nightmare-53b4103e89cb1.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "251000",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/nightmare-5be54c7b03578.jpg",
+ "likes": "0"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "11630",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/nightmare-4e8059a3c581c.png",
+ "likes": "2",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "6937c4d3-ecf8-3464-8453-9f3a86dbecef": {
+ "albumcover": [
+ {
+ "id": "190903",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/live-in-the-lbc--diamonds-in-the-rough-56daab95340aa.jpg",
+ "likes": "3"
+ },
+ {
+ "id": "249974",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/live-in-the-lbc--diamonds-in-the-rough-5bcd0ee12eecc.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "102876",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/live-in-the-lbc--diamonds-in-the-rough-526fdf214a2ba.jpg",
+ "likes": "0"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "105723",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/live-in-the-lbc--diamonds-in-the-rough-5296c531b6370.png",
+ "likes": "0",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "94672194-7f42-3965-a489-f2f3cdc1c79e": {
+ "albumcover": [
+ {
+ "id": "128449",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/avenged-sevenfold-53b3b73b9800f.jpg",
+ "likes": "3"
+ },
+ {
+ "id": "128450",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/avenged-sevenfold-53b3b749894c6.jpg",
+ "likes": "1"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "9923",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/avenged-sevenfold-4e5f7b9f5fb7f.png",
+ "likes": "0",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "180560ee-2d9d-33cf-8de7-cdaaba610739": {
+ "albumcover": [
+ {
+ "id": "128493",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/city-of-evil-53b40f8f72620.jpg",
+ "likes": "3"
+ },
+ {
+ "id": "168354",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/city-of-evil-559316eeccee8.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "128494",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/city-of-evil-53b40fa5e5385.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "250863",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/city-of-evil-5be135ee3e6c7.jpg",
+ "likes": "0"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "9921",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/city-of-evil-4e5f7b9f50d37.png",
+ "likes": "0",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "6f7e9796-bec7-4b79-aa6a-27db027f5c57": {
+ "albumcover": [
+ {
+ "id": "205688",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/the-stage-581358402bb49.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "249971",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/the-stage-5bcd0e5ef1df7.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "249975",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/the-stage-5bcd0ee132ed4.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "230472",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/the-stage-5a134ebfa84d0.jpg",
+ "likes": "0"
+ }
+ ]
+ },
+ "e7ea84a1-bf34-43f9-a421-af93624b708f": {
+ "albumcover": [
+ {
+ "id": "249976",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/all-excess-5bcd101139bc6.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "128401",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/all-excess-53b3086f398d7.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "78b8223a-8ba3-403e-bea6-63be9f378716": {
+ "albumcover": [
+ {
+ "id": "249977",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/critical-acclaim-5bcd114adbe6c.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "131968",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/critical-acclaim-53e65d0ba7dab.jpg",
+ "likes": "0"
+ }
+ ]
+ },
+ "46303229-3ef4-480d-b648-a78e1c64c911": {
+ "albumcover": [
+ {
+ "id": "128895",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-53b8592f20a8a.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "128896",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-53b85946c4a9a.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "190904",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-56daabbf72f39.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "128495",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-53b40fdfce251.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "128898",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-53b85961c471f.jpg",
+ "likes": "1"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "101408",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/hail-to-the-king-525c4d2d69b27.png",
+ "likes": "1",
+ "disc": "1",
+ "size": "1000"
+ },
+ {
+ "id": "98963",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/hail-to-the-king-523da0068ff85.png",
+ "likes": "0",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "9d642393-0005-3e89-b3d4-35d89c2f6ad6": {
+ "albumcover": [
+ {
+ "id": "128403",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/sounding-the-seventh-trumpet-53b308d7bea26.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "3031",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/sounding-the-seventh-trumpet-4ddd79ca1d05e.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "250992",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/sounding-the-seventh-trumpet-5be53d28d601e.jpg",
+ "likes": "0"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "105693",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/sounding-the-seventh-trumpet-529649562b8bf.png",
+ "likes": "2",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "1c7120ae-32b6-3693-8974-599977b01601": {
+ "albumcover": [
+ {
+ "id": "128452",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/waking-the-fallen-53b3b83b67dbe.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "131878",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/waking-the-fallen-53e524aeb7a1b.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "249972",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/waking-the-fallen-5bcd0e5f03053.jpg",
+ "likes": "1"
+ }
+ ],
+ "cdart": [
+ {
+ "id": "9922",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/cdart/waking-the-fallen-4e5f7b9f5ebdf.png",
+ "likes": "0",
+ "disc": "1",
+ "size": "1000"
+ }
+ ]
+ },
+ "46633632-801d-4f4d-8c5b-0d2a207ae2f8": {
+ "albumcover": [
+ {
+ "id": "128492",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/carry-on-call-of-duty-black-ops-ii-version-53b40f6ebbf21.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "190902",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/carry-on-call-of-duty-black-ops-ii-version-56daa99ff2f13.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "04576112-0cb5-381e-85f8-9020939de1a9": {
+ "albumcover": [
+ {
+ "id": "129360",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/beast-and-the-harlot-53bc6579556f5.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "7a64c494-4652-4577-93b3-0d6090f2473e": {
+ "albumcover": [
+ {
+ "id": "180078",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/hail-to-the-king-deathbat-original-video-game-soundtrack-560ceef32e364.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "b47f833c-f496-4a00-b9c4-7336a04fb867": {
+ "albumcover": [
+ {
+ "id": "249966",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/as-tears-go-by-5bcd0e5ed3742.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "811001d5-88bb-4ba2-bcef-05827d85d4f1": {
+ "albumcover": [
+ {
+ "id": "249967",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/dose-5bcd0e5ed762d.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "e0344128-b071-48eb-b420-3c5f8d9ea8cd": {
+ "albumcover": [
+ {
+ "id": "249968",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/god-only-knows-5bcd0e5edf7ca.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "d041c6c1-0829-4f6e-a69e-8e29ad004860": {
+ "albumcover": [
+ {
+ "id": "249969",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/mad-hatter-5bcd0e5ee776d.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "02ff3ae6-89b9-4a14-a147-d86e80a4cdad": {
+ "albumcover": [
+ {
+ "id": "249970",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/malaguea-salerosa-la-malaguea-5bcd0e5eeb939.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "123d6c7f-d26a-4b94-a79e-fcdec59ec0b9": {
+ "albumcover": [
+ {
+ "id": "249973",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/wish-you-were-here-5bcd0e5f079fc.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "5ddff4f2-c053-46ee-b785-180c06e5dd97": {
+ "albumcover": [
+ {
+ "id": "249979",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/the-best-of-2005-2013-5bcd16939221d.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "87c98289-1b38-4eb3-baf5-eb9f6f5a165d": {
+ "albumcover": [
+ {
+ "id": "249980",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/shepherd-of-fire-5bcd179e54c3b.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "adaa4143-984e-48a6-a94a-b8f48ea19614": {
+ "albumcover": [
+ {
+ "id": "128899",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/not-ready-to-die-from-call-of-the-dead-53b8597c550fd.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "7f017b86-bd88-3ed2-a16f-19d6427c423a": {
+ "albumcover": [
+ {
+ "id": "128402",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/walk-53b3089985c9f.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "fbbcda22-58a8-44fd-972d-e4d25e18e544": {
+ "albumcover": [
+ {
+ "id": "129203",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/so-far-away-53bac4f31dd95.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "f674046a-ea6e-4af5-a04e-82263702ec8d": {
+ "albumcover": [
+ {
+ "id": "129204",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/nightmare-53bac5ee6506d.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "da8d77cd-dd26-4356-86f9-9fe45b25f799": {
+ "albumcover": [
+ {
+ "id": "129206",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/crossroads-53bac65f5d21a.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "6292ac51-7acc-341c-a00a-bab0f21ab01b": {
+ "albumcover": [
+ {
+ "id": "129207",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/dear-god-53bac6c17e960.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "14d9ed56-09dd-4247-b5b2-add06260a79a": {
+ "albumcover": [
+ {
+ "id": "129208",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/afterlife-53bac73411c0d.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "f12684ec-4fef-4726-a1fe-8fa96a6bb1ce": {
+ "albumcover": [
+ {
+ "id": "128448",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/almost-easy-53b3b72787936.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "ceb758f6-aaa9-37a9-a32e-1c21127394e3": {
+ "albumcover": [
+ {
+ "id": "128451",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/burn-it-down-53b3b7ae7c0da.jpg",
+ "likes": "1"
+ }
+ ]
+ },
+ "9083fc88-0f05-4a30-b38d-a23e02dbb3d0": {
+ "albumcover": [
+ {
+ "id": "128453",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/welcome-to-the-family-53b3b876a15a9.jpeg",
+ "likes": "1"
+ }
+ ]
+ },
+ "93ed6b5b-818a-4755-8767-de707c9619b8": {
+ "albumcover": [
+ {
+ "id": "231762",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/live-at-the-grammy-museum-5a2eb8f73efad.jpg",
+ "likes": "0"
+ }
+ ]
+ },
+ "9c564498-48e5-4ee5-bb3a-dbc003cb36e2": {
+ "albumcover": [
+ {
+ "id": "248157",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/albumcover/black-reign-5ba7d9ae1b7ad.jpg",
+ "likes": "0"
+ }
+ ]
+ }
+ },
+ "hdmusiclogo": [
+ {
+ "id": "102865",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-526fc8c4162a1.png",
+ "likes": "2"
+ },
+ {
+ "id": "49644",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-503fcebece042.png",
+ "likes": "2"
+ },
+ {
+ "id": "252774",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-5c0de54aee1e1.png",
+ "likes": "1"
+ },
+ {
+ "id": "252775",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-5c0de55415caf.png",
+ "likes": "1"
+ },
+ {
+ "id": "249963",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-5bcd06e699335.png",
+ "likes": "1"
+ },
+ {
+ "id": "125845",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-538ce566b1184.png",
+ "likes": "1"
+ },
+ {
+ "id": "142153",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-54752ea02da53.png",
+ "likes": "0"
+ },
+ {
+ "id": "125844",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-538ce566a8f0a.png",
+ "likes": "0"
+ },
+ {
+ "id": "125888",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-538dcd706ac32.png",
+ "likes": "0"
+ },
+ {
+ "id": "102871",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-526fd948c757d.png",
+ "likes": "0"
+ },
+ {
+ "id": "49645",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/hdmusiclogo/avenged-sevenfold-503fcebecf17e.png",
+ "likes": "0"
+ }
+ ],
+ "artistthumb": [
+ {
+ "id": "95995",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistthumb/avenged-sevenfold-521f7ed77cf4f.jpg",
+ "likes": "2"
+ },
+ {
+ "id": "249978",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistthumb/avenged-sevenfold-5bcd121f3fda0.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "64042",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistthumb/avenged-sevenfold-50c4d9279d6e9.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "31109",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistthumb/avenged-sevenfold-4fb2b533bc73a.jpg",
+ "likes": "0"
+ }
+ ],
+ "artistbackground": [
+ {
+ "id": "64048",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-50c4dc653f004.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "3027",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-4ddd7889a0fcf.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "80600",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-517bf40c1fb22.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "105700",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-529652bade8a4.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "95993",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-521f7b1318390.jpg",
+ "likes": "1"
+ },
+ {
+ "id": "64046",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-50c4db9a2c6e2.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "76946",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-51544ff4401ab.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "105695",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-52964c30e0486.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "105697",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-52964cdfa6282.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "105698",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-52964d9c79f26.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "105702",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-529653ef8cef7.jpg",
+ "likes": "0"
+ },
+ {
+ "id": "95994",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/artistbackground/avenged-sevenfold-521f7e4c48ad3.jpg",
+ "likes": "0"
+ }
+ ],
+ "musiclogo": [
+ {
+ "id": "5712",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/musiclogo/avenged-sevenfold-4dfc8aee78b49.png",
+ "likes": "0"
+ },
+ {
+ "id": "41835",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/musiclogo/avenged-sevenfold-4ffc75f3a7e54.png",
+ "likes": "0"
+ },
+ {
+ "id": "41836",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/musiclogo/avenged-sevenfold-4ffc75f3a8473.png",
+ "likes": "0"
+ }
+ ],
+ "musicbanner": [
+ {
+ "id": "52630",
+ "url": "https://assets.fanart.tv/fanart/music/24e1b53c-3085-4581-8472-0b0088d2508c/musicbanner/avenged-sevenfold-505b2346a559d.jpg",
+ "likes": "0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/fanart/tests/response/tv_239761.json b/lib/fanart/tests/response/tv_239761.json
new file mode 100644
index 000000000..1db2a1514
--- /dev/null
+++ b/lib/fanart/tests/response/tv_239761.json
@@ -0,0 +1,256 @@
+{
+ "name": "Wilfred (US)",
+ "thetvdb_id": "239761",
+ "clearlogo": [
+ {
+ "id": "11977",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-4e04b6495dfd3.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "28249",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-517ac36e39f67.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "31817",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearlogo/wilfred-us-51f557082cfde.png",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "clearart": [
+ {
+ "id": "11987",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearart/wilfred-us-4e05f10e87711.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "12470",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/clearart/wilfred-us-4e2f151d5ed62.png",
+ "lang": "en",
+ "likes": "1"
+ }
+ ],
+ "hdtvlogo": [
+ {
+ "id": "21101",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-505f373be58e6.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "28248",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-517ac360def17.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "33750",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-52402df7ed945.png",
+ "lang": "he",
+ "likes": "1"
+ },
+ {
+ "id": "42207",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-53d12425b2d8c.png",
+ "lang": "ru",
+ "likes": "1"
+ },
+ {
+ "id": "31816",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdtvlogo/wilfred-us-51f556fb4abd3.png",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "tvthumb": [
+ {
+ "id": "19596",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvthumb/wilfred-us-501cf526174fe.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "30060",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvthumb/wilfred-us-51bfb4a105904.jpg",
+ "lang": "en",
+ "likes": "1"
+ }
+ ],
+ "hdclearart": [
+ {
+ "id": "21112",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdclearart/wilfred-us-505f94ed0ba13.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "33751",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/hdclearart/wilfred-us-52403264aa3ec.png",
+ "lang": "he",
+ "likes": "1"
+ }
+ ],
+ "seasonthumb": [
+ {
+ "id": "33752",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-52403782bab55.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "33753",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-5240379335232.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "2"
+ },
+ {
+ "id": "33754",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-524037bc83c7d.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "19586",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb0a8e60f9.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "19587",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb0b4bf229.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "19588",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-501bb144e6a46.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "0"
+ },
+ {
+ "id": "30309",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/seasonthumb/wilfred-us-51c953105ef77.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ }
+ ],
+ "tvbanner": [
+ {
+ "id": "33755",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52403a7185070.jpg",
+ "lang": "he",
+ "likes": "1"
+ },
+ {
+ "id": "57138",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5607140bac1c6.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "34716",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-5265193db51f7.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "36389",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578d57cb24.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "36390",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-52d578de27a66.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "41457",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da4cd2a21.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "41458",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53a4da6b594e5.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "41685",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7db8014425.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "41686",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-53b7dbb922332.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "57139",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvbanner/wilfred-us-56071512b14d6.jpg",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "tvposter": [
+ {
+ "id": "34584",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/tvposter/wilfred-us-525d893230d7c.jpg",
+ "lang": "he",
+ "likes": "1"
+ }
+ ],
+ "showbackground": [
+ {
+ "id": "19965",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-5034dbd49115e.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "23166",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92db6973.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "23167",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92dbb46b.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "23168",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-50b0c92dbb9d1.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "36386",
+ "url": "https://assets.fanart.tv/fanart/tv/239761/showbackground/wilfred-us-52d53d75d4782.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/fanart/tests/response/tv_79349.json b/lib/fanart/tests/response/tv_79349.json
new file mode 100644
index 000000000..81c347e3a
--- /dev/null
+++ b/lib/fanart/tests/response/tv_79349.json
@@ -0,0 +1,1408 @@
+{
+ "name": "Dexter",
+ "thetvdb_id": "79349",
+ "hdtvlogo": [
+ {
+ "id": "20959",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdtvlogo/dexter-50575994eb118.png",
+ "lang": "en",
+ "likes": "15"
+ },
+ {
+ "id": "20378",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdtvlogo/dexter-503fc2f24d9b3.png",
+ "lang": "en",
+ "likes": "9"
+ },
+ {
+ "id": "56928",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdtvlogo/dexter-560024323ccde.png",
+ "lang": "it",
+ "likes": "2"
+ },
+ {
+ "id": "33255",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdtvlogo/dexter-522612708ad8c.png",
+ "lang": "he",
+ "likes": "1"
+ }
+ ],
+ "hdclearart": [
+ {
+ "id": "23059",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-50af98e73b0a5.png",
+ "lang": "en",
+ "likes": "12"
+ },
+ {
+ "id": "24313",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-50eb4363da522.png",
+ "lang": "en",
+ "likes": "5"
+ },
+ {
+ "id": "20560",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-504775fd50557.png",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "29495",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51aa63100548b.png",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "26712",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51400b1672938.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "29496",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51aa724f0a2ab.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "29505",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51aab23851368.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "29594",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51afbcdf38d5e.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "29595",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-51afbcdf3ea8e.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "33257",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/hdclearart/dexter-52261505049ff.png",
+ "lang": "he",
+ "likes": "1"
+ }
+ ],
+ "seasonposter": [
+ {
+ "id": "39258",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d0ce4114d.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "1"
+ },
+ {
+ "id": "39259",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d0d949dec.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "2"
+ },
+ {
+ "id": "39260",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d0e277220.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "3"
+ },
+ {
+ "id": "39261",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d0ed6b9e7.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "4"
+ },
+ {
+ "id": "39262",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d0f6ccb3f.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "5"
+ },
+ {
+ "id": "39263",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d100194e2.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "6"
+ },
+ {
+ "id": "39264",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d10a3f9c6.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "7"
+ },
+ {
+ "id": "39265",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5352d1140a2c1.jpg",
+ "lang": "en",
+ "likes": "6",
+ "season": "8"
+ },
+ {
+ "id": "80646",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1c37c1831f7.jpg",
+ "lang": "es",
+ "likes": "2",
+ "season": "2"
+ },
+ {
+ "id": "80679",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1def03bbde1.jpg",
+ "lang": "es",
+ "likes": "2",
+ "season": "4"
+ },
+ {
+ "id": "80680",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1def1c0a3d6.jpg",
+ "lang": "es",
+ "likes": "2",
+ "season": "5"
+ },
+ {
+ "id": "39026",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53503b3a0f996.jpg",
+ "lang": "en",
+ "likes": "2",
+ "season": "1"
+ },
+ {
+ "id": "39027",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53503b5eb3b4f.jpg",
+ "lang": "en",
+ "likes": "2",
+ "season": "2"
+ },
+ {
+ "id": "83080",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a9c424eded47.jpg",
+ "lang": "en",
+ "likes": "2",
+ "season": "0"
+ },
+ {
+ "id": "38914",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd8c694015.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "38915",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd8fdc2efc.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "2"
+ },
+ {
+ "id": "38916",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd93a7e5f5.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "80645",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1c37adb9f0c.jpg",
+ "lang": "es",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "38917",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd96d73a88.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "4"
+ },
+ {
+ "id": "38918",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd9841a7f0.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "5"
+ },
+ {
+ "id": "38919",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd9a780844.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "6"
+ },
+ {
+ "id": "38920",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd9ba9b394.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "7"
+ },
+ {
+ "id": "38921",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-534cd9d1b434d.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "8"
+ },
+ {
+ "id": "80677",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1deeee90cda.jpg",
+ "lang": "es",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "80681",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5a1def2e96eb2.jpg",
+ "lang": "es",
+ "likes": "1",
+ "season": "6"
+ },
+ {
+ "id": "67422",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-582458e8cf1a1.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "67423",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5824590749728.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "67424",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5824592038b2d.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "67425",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5824593db6e54.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "67426",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-582459575f848.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "67427",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-5824597422014.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "6"
+ },
+ {
+ "id": "67428",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-582459a109f95.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "7"
+ },
+ {
+ "id": "67430",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-582459b84b922.jpg",
+ "lang": "it",
+ "likes": "0",
+ "season": "8"
+ },
+ {
+ "id": "39015",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502befabd2c.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "39016",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502c0f845b5.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "39017",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502c2757c78.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "39018",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502e082e6df.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "39019",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502e4d5a5ee.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "39020",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502e7fe0fc4.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "6"
+ },
+ {
+ "id": "39024",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonposter/dexter-53502f33bca2f.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "7"
+ }
+ ],
+ "characterart": [
+ {
+ "id": "16825",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-4f76318ae4410.png",
+ "lang": "en",
+ "likes": "6"
+ },
+ {
+ "id": "29497",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-51aa726346bcf.png",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "14981",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-4eface5cee809.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "29598",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-51afbcf6006e6.png",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "26713",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-51400b26c65de.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "16996",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-4f8189d220d4b.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "29597",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-51afbcf6002a7.png",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "29646",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/characterart/dexter-51b0fc45e0dc0.png",
+ "lang": "en",
+ "likes": "1"
+ }
+ ],
+ "clearlogo": [
+ {
+ "id": "20958",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearlogo/dexter-5057573260826.png",
+ "lang": "en",
+ "likes": "6"
+ },
+ {
+ "id": "2114",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearlogo/Dexter-79349-2.png",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "14577",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearlogo/dexter-4ecdf0c030189.png",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "16685",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearlogo/dexter-4f6879db58edf.png",
+ "lang": "ru",
+ "likes": "1"
+ }
+ ],
+ "tvposter": [
+ {
+ "id": "34331",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-5251f81d6a966.jpg",
+ "lang": "en",
+ "likes": "5"
+ },
+ {
+ "id": "32420",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-521277d441aa9.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "39863",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-535f654d173f2.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "44347",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-5402ddeace245.jpg",
+ "lang": "de",
+ "likes": "2"
+ },
+ {
+ "id": "33256",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-522613e9169f5.jpg",
+ "lang": "he",
+ "likes": "2"
+ },
+ {
+ "id": "67896",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-583183afab5dc.jpg",
+ "lang": "00",
+ "likes": "1"
+ },
+ {
+ "id": "67898",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-583183afac80b.jpg",
+ "lang": "00",
+ "likes": "1"
+ },
+ {
+ "id": "67908",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-583183c32c9fe.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "67914",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-5831957d46802.jpg",
+ "lang": "00",
+ "likes": "1"
+ },
+ {
+ "id": "83079",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-5a9c404413241.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "78406",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvposter/dexter-59b19badbee8a.jpg",
+ "lang": "00",
+ "likes": "0"
+ }
+ ],
+ "showbackground": [
+ {
+ "id": "18467",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fc683691dea7.jpg",
+ "lang": "",
+ "likes": "5",
+ "season": "1"
+ },
+ {
+ "id": "18468",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fc683a5ab451.jpg",
+ "lang": "",
+ "likes": "3",
+ "season": "6"
+ },
+ {
+ "id": "67878",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539d11d.jpg",
+ "lang": "",
+ "likes": "3",
+ "season": "all"
+ },
+ {
+ "id": "67884",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539fb9b.jpg",
+ "lang": "",
+ "likes": "3",
+ "season": "all"
+ },
+ {
+ "id": "18950",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fdf608e2df53.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "3"
+ },
+ {
+ "id": "21524",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-506bdd9c35771.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "21526",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-506bddc9f04cb.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "21527",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-506bdddc3f476.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "67877",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539c2d2.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "67881",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539e86a.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "67882",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539ef39.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "67883",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539f56e.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "83084",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5a9c5a53e8c61.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "83085",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5a9c5a53f009e.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "83086",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5a9c5a5403ddd.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "32193",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-520e19dd4de8c.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "24046",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50de1f84e736f.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "24055",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50def777e8dbc.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "24058",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50def777ea9c8.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "34303",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5250cd641c716.jpg",
+ "lang": "",
+ "likes": "2",
+ "season": "all"
+ },
+ {
+ "id": "18952",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fdf6386ce1c1.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "21525",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-506bddb3bd3f4.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "18466",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fc6830dc2ccc.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "4"
+ },
+ {
+ "id": "67880",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-583183539e166.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "24049",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50de21ac3ae25.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "24054",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50def777e84d0.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "36343",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-52d406b724ab8.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "24056",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50def777e9762.jpg",
+ "lang": "",
+ "likes": "1",
+ "season": "all"
+ },
+ {
+ "id": "18947",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fdf5e107be0d.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "18949",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fdf601385517.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "21529",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-506bde113406e.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "18515",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-4fc8eab16803c.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "24986",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5101fa187c857.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "33202",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-5224d4d8b7b2a.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "24048",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/showbackground/dexter-50de1f84e7d57.jpg",
+ "lang": "",
+ "likes": "0",
+ "season": "all"
+ }
+ ],
+ "clearart": [
+ {
+ "id": "4980",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/D_79349 (3).png",
+ "lang": "en",
+ "likes": "5"
+ },
+ {
+ "id": "14579",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/dexter-4ecdf0db2adf1.png",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "16682",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/dexter-4f68753540f2d.png",
+ "lang": "ru",
+ "likes": "1"
+ },
+ {
+ "id": "17196",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/dexter-4f8af83f3bde7.png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "4982",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/D_79349.png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "4983",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/D_79349 (1).png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "4984",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/D_79349 (0).png",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "14578",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/clearart/dexter-4ecdf0cf3fb38.png",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "tvthumb": [
+ {
+ "id": "31722",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-51f27112a2a89.jpg",
+ "lang": "en",
+ "likes": "4"
+ },
+ {
+ "id": "67892",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-58318380c7599.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "5012",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (10).jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "29341",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-51a338d376b4a.jpg",
+ "lang": "de",
+ "likes": "2"
+ },
+ {
+ "id": "5023",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (0).jpg",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "14580",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-4ecdf5027a53c.jpg",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "67889",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-58318380c5b8d.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "67890",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-58318380c68a8.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "67893",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-58318380c7be8.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "5013",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (9).jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "5016",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (6).jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "5020",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (2).jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "14277",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-4ead4375923fd.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "33261",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/dexter-522648127939e.jpg",
+ "lang": "he",
+ "likes": "1"
+ },
+ {
+ "id": "5010",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (12).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5011",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (11).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5014",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (8).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5015",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (7).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5017",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (5).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5018",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (4).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5019",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (3).jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5021",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "5022",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvthumb/D_79349 (1).jpg",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "seasonthumb": [
+ {
+ "id": "18986",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21b7708ebe.jpg",
+ "lang": "en",
+ "likes": "3",
+ "season": "6"
+ },
+ {
+ "id": "35585",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-52aa23371bfd1.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "8"
+ },
+ {
+ "id": "35586",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-52aa2340b016e.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "8"
+ },
+ {
+ "id": "18980",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21a6955116.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "18982",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21b0767edb.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "2"
+ },
+ {
+ "id": "18983",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21b292d661.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "18984",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21b42d983d.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "4"
+ },
+ {
+ "id": "18985",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fe21b5847d7b.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "5"
+ },
+ {
+ "id": "21883",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-5071800d37e80.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "7"
+ },
+ {
+ "id": "5002",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (6).jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "17802",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa981a7251d7.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "5"
+ },
+ {
+ "id": "5003",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (5).jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "17823",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4faab0bccbfb6.jpg",
+ "lang": "en",
+ "likes": "1",
+ "season": "6"
+ },
+ {
+ "id": "33262",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-522648bfbfe72.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "1"
+ },
+ {
+ "id": "33263",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-522648ff989df.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "2"
+ },
+ {
+ "id": "33264",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-5226492b7f2ff.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "3"
+ },
+ {
+ "id": "33265",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-522649758054c.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "4"
+ },
+ {
+ "id": "33266",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-522649ccebbfe.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "5"
+ },
+ {
+ "id": "33267",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-52264a005f5de.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "6"
+ },
+ {
+ "id": "33268",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-52264a5763db2.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "7"
+ },
+ {
+ "id": "33269",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-52264acd22558.jpg",
+ "lang": "he",
+ "likes": "1",
+ "season": "8"
+ },
+ {
+ "id": "31022",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-51dc720661cb7.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "8"
+ },
+ {
+ "id": "31023",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-51dc72a19a0bb.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "8"
+ },
+ {
+ "id": "18514",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fc8e9fa79bf8.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "7"
+ },
+ {
+ "id": "4989",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (9).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "4990",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (19).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "4991",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (18).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "4992",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (17).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "4993",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (16).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "4994",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (15).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "4995",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (14).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "4996",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (13).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "4997",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (12).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "4998",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (11).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "4999",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (10).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "5000",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (8).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "5001",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (7).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "17803",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa981a7258fb.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "5004",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "all"
+ },
+ {
+ "id": "17804",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa981a725c14.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "5005",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (4).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "17805",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa981c6607e4.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "0"
+ },
+ {
+ "id": "5006",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (3).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "5007",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (2).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "17807",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa98ac2b811d.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "6"
+ },
+ {
+ "id": "5008",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (1).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "17808",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa98ac2b87ab.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "6"
+ },
+ {
+ "id": "5009",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/Dexter (0).jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "17810",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonthumb/dexter-4fa994697afa3.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "6"
+ }
+ ],
+ "tvbanner": [
+ {
+ "id": "30063",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-51bfc89667267.jpg",
+ "lang": "en",
+ "likes": "3"
+ },
+ {
+ "id": "55124",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-55a6a0c9741cf.jpg",
+ "lang": "en",
+ "likes": "2"
+ },
+ {
+ "id": "30062",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-51bfc857c84fd.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "54982",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-55a26f57b2875.jpg",
+ "lang": "en",
+ "likes": "1"
+ },
+ {
+ "id": "33270",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-52264bbad5b81.jpg",
+ "lang": "he",
+ "likes": "1"
+ },
+ {
+ "id": "35192",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-527d5d075dc35.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "35193",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-527d5d18dee23.jpg",
+ "lang": "en",
+ "likes": "0"
+ },
+ {
+ "id": "34785",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/tvbanner/dexter-52670388cd091.jpg",
+ "lang": "en",
+ "likes": "0"
+ }
+ ],
+ "seasonbanner": [
+ {
+ "id": "38922",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda15476b0.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "1"
+ },
+ {
+ "id": "38923",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda21d67f4.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "2"
+ },
+ {
+ "id": "38924",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda2c573e3.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "3"
+ },
+ {
+ "id": "38925",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda37326a0.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "4"
+ },
+ {
+ "id": "38926",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda40849d6.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "5"
+ },
+ {
+ "id": "38927",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda4d0c8bb.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "6"
+ },
+ {
+ "id": "38928",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda594e7ff.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "7"
+ },
+ {
+ "id": "38929",
+ "url": "https://assets.fanart.tv/fanart/tv/79349/seasonbanner/dexter-534cda670afbe.jpg",
+ "lang": "en",
+ "likes": "0",
+ "season": "8"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/fanart/tests/test_core.py b/lib/fanart/tests/test_core.py
new file mode 100644
index 000000000..e7483c722
--- /dev/null
+++ b/lib/fanart/tests/test_core.py
@@ -0,0 +1,25 @@
+from unittest import TestCase
+from fanart.core import Request
+from fanart.errors import RequestFanartError, ResponseFanartError
+from httpretty import httprettified, HTTPretty
+
+
+class RequestTestCase(TestCase):
+ def test_valitate_error(self):
+ self.assertRaises(RequestFanartError, Request, 'key', 'id', 'sport')
+
+ @httprettified
+ def test_response_error(self):
+ request = Request('apikey', 'objid', 'tv')
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/tv/objid?api_key=apikey',
+ body='Please specify a valid API key',
+ )
+ try:
+ request.response()
+ except ResponseFanartError as e:
+ self.assertEqual(repr(e), "ResponseFanartError('Expecting value: "
+ "line 1 column 1 (char 0)',)")
+ self.assertEqual(str(e), "Expecting value: "
+ "line 1 column 1 (char 0)")
diff --git a/lib/fanart/tests/test_immutable.py b/lib/fanart/tests/test_immutable.py
new file mode 100644
index 000000000..8a0149dbe
--- /dev/null
+++ b/lib/fanart/tests/test_immutable.py
@@ -0,0 +1,49 @@
+from unittest import TestCase
+from fanart.immutable import Immutable
+
+
+class TestMutable(object):
+ def __init__(self, spam, ham, eggs):
+ self.spam = spam
+ self.ham = ham
+ self.eggs = eggs
+
+ @Immutable.mutablemethod
+ def anyway(self):
+ self.spam = self.ham + self.eggs
+
+
+class TestImmutable(TestMutable, Immutable):
+ @Immutable.mutablemethod
+ def __init__(self, *args, **kwargs):
+ super(TestImmutable, self).__init__(*args, **kwargs)
+
+
+class ImmutableTestCase(TestCase):
+ def setUp(self):
+ self.instance = TestImmutable('spam', 'ham', 'eggs')
+
+ def test_set_raises(self):
+ self.assertRaises(TypeError, self.instance.__setattr__, 'spam', 'ham')
+
+ def test_set(self):
+ self.instance._mutable = True
+ self.instance.spam = 'ham'
+ self.assertEqual(self.instance.spam, 'ham')
+
+ def test_del_raises(self):
+ self.assertRaises(TypeError, self.instance.__delattr__, 'spam')
+
+ def test_del(self):
+ self.instance._mutable = True
+ del self.instance.spam
+ self.assertRaises(AttributeError, self.instance.__getattribute__, 'spam')
+
+ def test_equal(self):
+ new_instance = TestImmutable('spam', 'ham', 'eggs')
+ self.assertEqual(self.instance, new_instance)
+
+ def test_mutable_dec(self):
+ instance = TestMutable('spam', 'ham', 'eggs')
+ instance.anyway()
+ self.assertEqual(instance.spam, 'hameggs')
diff --git a/lib/fanart/tests/test_items.py b/lib/fanart/tests/test_items.py
new file mode 100644
index 000000000..12bb3d1d0
--- /dev/null
+++ b/lib/fanart/tests/test_items.py
@@ -0,0 +1,27 @@
+from unittest import TestCase
+import os
+from fanart.items import LeafItem
+from httpretty import httprettified, HTTPretty
+from fanart.tests import LOCALDIR
+
+
+class LeafItemTestCase(TestCase):
+ def setUp(self):
+ self.leaf = LeafItem(id=11977, likes=2, url='http://test.tv/50x50.txt')
+
+ def test_str(self):
+ self.assertEqual(str(self.leaf), 'http://test.tv/50x50.txt')
+
+ @httprettified
+ def test_content(self):
+ with open(os.path.join(LOCALDIR, 'response/50x50.png'), 'rb') as fp:
+ body = fp.read()
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://test.tv/50x50.txt',
+ body=body
+ )
+ self.assertEqual(self.leaf.content(), body)
+ self.assertEqual(len(HTTPretty.latest_requests), 1)
+ self.assertEqual(self.leaf.content(), body) # Cached
+ self.assertEqual(len(HTTPretty.latest_requests), 1)
diff --git a/lib/fanart/tests/test_movie.py b/lib/fanart/tests/test_movie.py
new file mode 100644
index 000000000..4fdac095a
--- /dev/null
+++ b/lib/fanart/tests/test_movie.py
@@ -0,0 +1,22 @@
+import os
+import unittest
+from httpretty import HTTPretty, httprettified
+from fanart.movie import *
+from fanart.tests import LOCALDIR
+os.environ['FANART_APIKEY'] = 'e3c7f0d0beeaf45b3a0dd3b9dd8a3338'
+
+
+class TvItemTestCase(unittest.TestCase):
+ @httprettified
+ def test_get(self):
+ with open(os.path.join(LOCALDIR, 'response/movie_thg.json')) as fp:
+ body = fp.read()
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/movies/70160?api_key={}'.format(
+ os.environ['FANART_APIKEY']),
+ body=body
+ )
+ hunger_games = Movie.get(id=70160)
+ self.assertEqual(hunger_games.tmdbid, '70160')
+ self.assertEqual(hunger_games, eval(repr(hunger_games)))
diff --git a/lib/fanart/tests/test_music.py b/lib/fanart/tests/test_music.py
new file mode 100644
index 000000000..d35705bcc
--- /dev/null
+++ b/lib/fanart/tests/test_music.py
@@ -0,0 +1,24 @@
+import os
+import unittest
+from httpretty import HTTPretty, httprettified
+from fanart.music import *
+from fanart.tests import LOCALDIR
+os.environ['FANART_APIKEY'] = 'e3c7f0d0beeaf45b3a0dd3b9dd8a3338'
+
+
+class ArtistItemTestCase(unittest.TestCase):
+ @httprettified
+ def test_get(self):
+ artist_id = '24e1b53c-3085-4581-8472-0b0088d2508c'
+ with open(os.path.join(LOCALDIR, 'response/music_a7f.json')) as fp:
+ body = fp.read()
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/music/{}?api_key={}'.format(
+ artist_id, os.environ['FANART_APIKEY']),
+ body=body
+ )
+ a7f = Artist.get(id=artist_id)
+ self.assertEqual(a7f.mbid, artist_id)
+ self.assertEqual(a7f, eval(repr(a7f)))
+ self.assertEqual(len(a7f.thumbs), 4)
diff --git a/lib/fanart/tests/test_tv.py b/lib/fanart/tests/test_tv.py
new file mode 100644
index 000000000..1b9bddd7a
--- /dev/null
+++ b/lib/fanart/tests/test_tv.py
@@ -0,0 +1,54 @@
+import json
+from fanart.errors import ResponseFanartError
+import os
+import unittest
+from httpretty import HTTPretty, httprettified
+from fanart.tv import *
+from fanart.tests import LOCALDIR
+os.environ['FANART_APIKEY'] = 'e3c7f0d0beeaf45b3a0dd3b9dd8a3338'
+
+
+class TvItemTestCase(unittest.TestCase):
+ @httprettified
+ def test_get_wilfred(self):
+ with open(os.path.join(LOCALDIR, 'response/tv_239761.json')) as fp:
+ body = fp.read()
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/tv/239761?api_key={}'.format(
+ os.environ['FANART_APIKEY']),
+ body=body
+ )
+ wilfred = TvShow.get(id=239761)
+
+ # If we update `response/tv_239761.json`, then we also must update
+ # `json/wilfred.json`, and to do so, we can use the following command:
+ # print(wilfred.json(indent=4))
+
+ self.assertEqual(wilfred.tvdbid, '239761')
+ with open(os.path.join(LOCALDIR, 'json/wilfred.json')) as fp:
+ self.assertEqual(json.loads(wilfred.json()), json.load(fp))
+
+ @httprettified
+ def test_get_dexter(self):
+ with open(os.path.join(LOCALDIR, 'response/tv_79349.json')) as fp:
+ body = fp.read()
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/tv/79349?api_key={}'.format(
+ os.environ['FANART_APIKEY']),
+ body=body
+ )
+ dexter = TvShow.get(id=79349)
+ self.assertEqual(dexter.tvdbid, '79349')
+ self.assertEqual(dexter, eval(repr(dexter)))
+
+ @httprettified
+ def test_get_null(self):
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ 'http://webservice.fanart.tv/v3/tv/79349?api_key={}'.format(
+ os.environ['FANART_APIKEY']),
+ body='null'
+ )
+ self.assertRaises(ResponseFanartError, TvShow.get, id=79349)
diff --git a/lib/fanart/tv.py b/lib/fanart/tv.py
new file mode 100644
index 000000000..d50c64b56
--- /dev/null
+++ b/lib/fanart/tv.py
@@ -0,0 +1,124 @@
+import fanart
+from fanart.items import LeafItem, Immutable, ResourceItem
+__all__ = (
+ 'CharacterItem',
+ 'ArtItem',
+ 'LogoItem',
+ 'BackgroundItem',
+ 'SeasonItem',
+ 'SeasonPosterItem',
+ 'SeasonBannerItem',
+ 'ThumbItem',
+ 'HdLogoItem',
+ 'HdArtItem',
+ 'PosterItem',
+ 'BannerItem',
+ 'TvShow',
+)
+
+
+class TvItem(LeafItem):
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes, lang):
+ super(TvItem, self).__init__(id, url, likes)
+ self.lang = lang
+
+
+class SeasonedTvItem(TvItem):
+ @Immutable.mutablemethod
+ def __init__(self, id, url, likes, lang, season):
+ super(SeasonedTvItem, self).__init__(id, url, likes, lang)
+ self.season = 0 if season == 'all' else int(season or 0)
+
+
+class CharacterItem(TvItem):
+ KEY = fanart.TYPE.TV.CHARACTER
+
+
+class ArtItem(TvItem):
+ KEY = fanart.TYPE.TV.ART
+
+
+class LogoItem(TvItem):
+ KEY = fanart.TYPE.TV.LOGO
+
+
+class BackgroundItem(SeasonedTvItem):
+ KEY = fanart.TYPE.TV.BACKGROUND
+
+
+class SeasonItem(SeasonedTvItem):
+ KEY = fanart.TYPE.TV.SEASONTHUMB
+
+
+class SeasonPosterItem(SeasonedTvItem):
+ KEY = fanart.TYPE.TV.SEASONPOSTER
+
+
+class SeasonBannerItem(SeasonedTvItem):
+ KEY = fanart.TYPE.TV.SEASONBANNER
+
+
+class ThumbItem(TvItem):
+ KEY = fanart.TYPE.TV.THUMB
+
+
+class HdLogoItem(TvItem):
+ KEY = fanart.TYPE.TV.HDLOGO
+
+
+class HdArtItem(TvItem):
+ KEY = fanart.TYPE.TV.HDART
+
+
+class PosterItem(TvItem):
+ KEY = fanart.TYPE.TV.POSTER
+
+
+class BannerItem(TvItem):
+ KEY = fanart.TYPE.TV.BANNER
+
+
+class TvShow(ResourceItem):
+ WS = fanart.WS.TV
+
+ @Immutable.mutablemethod
+ def __init__(
+ self, name, tvdbid, backgrounds, characters, arts, logos,
+ seasons, thumbs, hdlogos, hdarts, posters, banners,
+ season_posters, season_banners):
+ self.name = name
+ self.tvdbid = tvdbid
+ self.backgrounds = backgrounds
+ self.characters = characters
+ self.arts = arts
+ self.logos = logos
+ self.seasons = seasons
+ self.thumbs = thumbs
+ self.hdlogos = hdlogos
+ self.hdarts = hdarts
+ self.posters = posters
+ self.banners = banners
+ self.season_posters = posters
+ self.season_banners = banners
+
+ @classmethod
+ def from_dict(cls, resource):
+ minimal_keys = {'name', 'thetvdb_id'}
+ assert all(k in resource for k in minimal_keys), 'Bad Format Map'
+ return cls(
+ name=resource['name'],
+ tvdbid=resource['thetvdb_id'],
+ backgrounds=BackgroundItem.extract(resource),
+ characters=CharacterItem.extract(resource),
+ arts=ArtItem.extract(resource),
+ logos=LogoItem.extract(resource),
+ seasons=SeasonItem.extract(resource),
+ thumbs=ThumbItem.extract(resource),
+ hdlogos=HdLogoItem.extract(resource),
+ hdarts=HdArtItem.extract(resource),
+ posters=PosterItem.extract(resource),
+ banners=BannerItem.extract(resource),
+ season_posters=SeasonPosterItem.extract(resource),
+ season_banners=SeasonBannerItem.extract(resource),
+ )
diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py
index e56e67cec..f477924db 100755
--- a/lib/musicbrainzngs/musicbrainz.py
+++ b/lib/musicbrainzngs/musicbrainz.py
@@ -20,6 +20,9 @@
from musicbrainzngs import util
from musicbrainzngs import compat
+import headphones
+from headphones import logger
+
# headphones
import base64
@@ -166,62 +169,62 @@ class AUTH_IFSET: pass
# Exceptions.
class MusicBrainzError(Exception):
- """Base class for all exceptions related to MusicBrainz."""
- pass
+ """Base class for all exceptions related to MusicBrainz."""
+ pass
class UsageError(MusicBrainzError):
- """Error related to misuse of the module API."""
- pass
+ """Error related to misuse of the module API."""
+ pass
class InvalidSearchFieldError(UsageError):
- pass
+ pass
class InvalidIncludeError(UsageError):
- def __init__(self, msg='Invalid Includes', reason=None):
- super(InvalidIncludeError, self).__init__(self)
- self.msg = msg
- self.reason = reason
+ def __init__(self, msg='Invalid Includes', reason=None):
+ super(InvalidIncludeError, self).__init__(self)
+ self.msg = msg
+ self.reason = reason
- def __str__(self):
- return self.msg
+ def __str__(self):
+ return self.msg
class InvalidFilterError(UsageError):
- def __init__(self, msg='Invalid Includes', reason=None):
- super(InvalidFilterError, self).__init__(self)
- self.msg = msg
- self.reason = reason
+ def __init__(self, msg='Invalid Includes', reason=None):
+ super(InvalidFilterError, self).__init__(self)
+ self.msg = msg
+ self.reason = reason
- def __str__(self):
- return self.msg
+ def __str__(self):
+ return self.msg
class WebServiceError(MusicBrainzError):
- """Error related to MusicBrainz API requests."""
- def __init__(self, message=None, cause=None):
- """Pass ``cause`` if this exception was caused by another
- exception.
- """
- self.message = message
- self.cause = cause
-
- def __str__(self):
- if self.message:
- msg = "%s, " % self.message
- else:
- msg = ""
- msg += "caused by: %s" % str(self.cause)
- return msg
+ """Error related to MusicBrainz API requests."""
+ def __init__(self, message=None, cause=None):
+ """Pass ``cause`` if this exception was caused by another
+ exception.
+ """
+ self.message = message
+ self.cause = cause
+
+ def __str__(self):
+ if self.message:
+ msg = "%s, " % self.message
+ else:
+ msg = ""
+ msg += "caused by: %s" % str(self.cause)
+ return msg
class NetworkError(WebServiceError):
- """Problem communicating with the MB server."""
- pass
+ """Problem communicating with the MB server."""
+ pass
class ResponseError(WebServiceError):
- """Bad response sent by the MB server."""
- pass
+ """Bad response sent by the MB server."""
+ pass
class AuthenticationError(WebServiceError):
- """Received a HTTP 401 response while accessing a protected resource."""
- pass
+ """Received a HTTP 401 response while accessing a protected resource."""
+ pass
# Helpers for validating and formatting allowed sets.
@@ -235,9 +238,9 @@ def _check_includes(entity, inc):
_check_includes_impl(inc, VALID_INCLUDES[entity])
def _check_filter(values, valid):
- for v in values:
- if v not in valid:
- raise InvalidFilterError(v)
+ for v in values:
+ if v not in valid:
+ raise InvalidFilterError(v)
def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[]):
"""Check that the status or type values are valid. Then, check that
@@ -302,12 +305,12 @@ def _decorator(func):
mb_auth = False
def auth(u, p):
- """Set the username and password to be used in subsequent queries to
- the MusicBrainz XML API that require authentication.
- """
- global user, password
- user = u
- password = p
+ """Set the username and password to be used in subsequent queries to
+ the MusicBrainz XML API that require authentication.
+ """
+ global user, password
+ user = u
+ password = p
# headphones
def hpauth(u, p):
@@ -331,10 +334,13 @@ def set_useragent(app, version, contact=None):
global _useragent, _client
if not app or not version:
raise ValueError("App and version can not be empty")
- if contact is not None:
- _useragent = "%s/%s python-musicbrainzngs/%s ( %s )" % (app, version, _version, contact)
+ if headphones.CONFIG.MUSICBRAINZ_USERAGENT:
+ _useragent = headphones.CONFIG.MUSICBRAINZ_USERAGENT
else:
- _useragent = "%s/%s python-musicbrainzngs/%s" % (app, version, _version)
+ if contact is not None:
+ _useragent = "%s/%s python-musicbrainzngs/%s ( %s )" % (app, version, _version, contact)
+ else:
+ _useragent = "%s/%s python-musicbrainzngs/%s" % (app, version, _version)
_client = "%s-%s" % (app, version)
_log.debug("set user-agent to %s" % _useragent)
@@ -422,19 +428,19 @@ def __call__(self, *args, **kwargs):
# From pymb2
class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
- def __init__(self):
- self._realms = { }
+ def __init__(self):
+ self._realms = { }
- def find_user_password(self, realm, uri):
- # ignoring the uri parameter intentionally
- try:
- return self._realms[realm]
- except KeyError:
- return (None, None)
+ def find_user_password(self, realm, uri):
+ # ignoring the uri parameter intentionally
+ try:
+ return self._realms[realm]
+ except KeyError:
+ return (None, None)
- def add_password(self, realm, uri, username, password):
- # ignoring the uri parameter intentionally
- self._realms[realm] = (username, password)
+ def add_password(self, realm, uri, username, password):
+ # ignoring the uri parameter intentionally
+ self._realms[realm] = (username, password)
class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
def get_authorization (self, req, chal):
@@ -468,84 +474,85 @@ def get_algorithm_impls(self, algorithm):
return H, KD
class _MusicbrainzHttpRequest(compat.Request):
- """ A custom request handler that allows DELETE and PUT"""
- def __init__(self, method, url, data=None):
- compat.Request.__init__(self, url, data)
- allowed_m = ["GET", "POST", "DELETE", "PUT"]
- if method not in allowed_m:
- raise ValueError("invalid method: %s" % method)
- self.method = method
+ """ A custom request handler that allows DELETE and PUT"""
+ def __init__(self, method, url, data=None):
+ compat.Request.__init__(self, url, data)
+ allowed_m = ["GET", "POST", "DELETE", "PUT"]
+ if method not in allowed_m:
+ raise ValueError("invalid method: %s" % method)
+ self.method = method
- def get_method(self):
- return self.method
+ def get_method(self):
+ return self.method
# Core (internal) functions for calling the MB API.
def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
- """Open an HTTP request with a given URL opener and (optionally) a
- request body. Transient errors lead to retries. Permanent errors
- and repeated errors are translated into a small set of handleable
- exceptions. Return a bytestring.
- """
- last_exc = None
- for retry_num in range(max_retries):
- if retry_num: # Not the first try: delay an increasing amount.
- _log.info("retrying after delay (#%i)" % retry_num)
- time.sleep(retry_num * retry_delay_delta)
-
- try:
- if body:
- f = opener.open(req, body)
- else:
- f = opener.open(req)
- return f.read()
-
- except compat.HTTPError as exc:
- if exc.code in (400, 404, 411):
- # Bad request, not found, etc.
- raise ResponseError(cause=exc)
- elif exc.code in (503, 502, 500):
- # Rate limiting, internal overloading...
- _log.info("HTTP error %i" % exc.code)
- elif exc.code in (401, ):
- raise AuthenticationError(cause=exc)
- else:
- # Other, unknown error. Should handle more cases, but
- # retrying for now.
- _log.info("unknown HTTP error %i" % exc.code)
- last_exc = exc
- except compat.BadStatusLine as exc:
- _log.info("bad status line")
- last_exc = exc
- except compat.HTTPException as exc:
- _log.info("miscellaneous HTTP exception: %s" % str(exc))
- last_exc = exc
- except compat.URLError as exc:
- if isinstance(exc.reason, socket.error):
- code = exc.reason.errno
- if code == 104: # "Connection reset by peer."
- continue
- raise NetworkError(cause=exc)
- except socket.timeout as exc:
- _log.info("socket timeout")
- last_exc = exc
- except socket.error as exc:
- if exc.errno == 104:
- continue
- raise NetworkError(cause=exc)
- except IOError as exc:
- raise NetworkError(cause=exc)
-
- # Out of retries!
- raise NetworkError("retried %i times" % max_retries, last_exc)
+ """Open an HTTP request with a given URL opener and (optionally) a
+ request body. Transient errors lead to retries. Permanent errors
+ and repeated errors are translated into a small set of handleable
+ exceptions. Return a bytestring.
+ """
+ last_exc = None
+ for retry_num in range(max_retries):
+ if retry_num: # Not the first try: delay an increasing amount.
+ _log.info("retrying after delay (#%i)" % retry_num)
+ time.sleep(retry_num * retry_delay_delta)
+
+ try:
+ if body:
+ f = opener.open(req, body)
+ else:
+ f = opener.open(req)
+ return f.read()
+
+ except compat.HTTPError as exc:
+ logger.error("Musicbrainz request error: HTTP code %s" % exc.code)
+ if exc.code in (400, 404, 411):
+ # Bad request, not found, etc.
+ raise ResponseError(cause=exc)
+ elif exc.code in (503, 502, 500):
+ # Rate limiting, internal overloading...
+ _log.info("HTTP error %i" % exc.code)
+ elif exc.code in (401, ):
+ raise AuthenticationError(cause=exc)
+ else:
+ # Other, unknown error. Should handle more cases, but
+ # retrying for now.
+ _log.info("unknown HTTP error %i" % exc.code)
+ last_exc = exc
+ except compat.BadStatusLine as exc:
+ _log.info("bad status line")
+ last_exc = exc
+ except compat.HTTPException as exc:
+ _log.info("miscellaneous HTTP exception: %s" % str(exc))
+ last_exc = exc
+ except compat.URLError as exc:
+ if isinstance(exc.reason, socket.error):
+ code = exc.reason.errno
+ if code == 104: # "Connection reset by peer."
+ continue
+ raise NetworkError(cause=exc)
+ except socket.timeout as exc:
+ _log.info("socket timeout")
+ last_exc = exc
+ except socket.error as exc:
+ if exc.errno == 104:
+ continue
+ raise NetworkError(cause=exc)
+ except IOError as exc:
+ raise NetworkError(cause=exc)
+
+ # Out of retries!
+ raise NetworkError("retried %i times" % max_retries, last_exc)
# Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
# and ElementTree 1.3.
if hasattr(etree, 'ParseError'):
- ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
+ ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
else:
- ETREE_EXCEPTIONS = (expat.ExpatError)
+ ETREE_EXCEPTIONS = (expat.ExpatError)
# Parsing setup
@@ -684,6 +691,7 @@ def _mb_request(path, method='GET', auth_required=AUTH_NO,
# Make request.
req = _MusicbrainzHttpRequest(method, url, data)
req.add_header('User-Agent', _useragent)
+ logger.debug("Musicbrainz request url: %s" % url)
# Add headphones credentials
if mb_auth:
@@ -691,6 +699,7 @@ def _mb_request(path, method='GET', auth_required=AUTH_NO,
req.add_header("Authorization", "Basic %s" % base64string)
_log.debug("requesting with UA %s" % _useragent)
+ logger.debug("Musicbrainz user-agent: %s" % _useragent)
if body:
req.add_header('Content-Type', 'application/xml; charset=UTF-8')
elif not data and not req.has_header('Content-Length'):
@@ -716,100 +725,100 @@ def _get_auth_type(entity, id, includes):
return AUTH_NO
def _do_mb_query(entity, id, includes=[], params={}):
- """Make a single GET call to the MusicBrainz XML API. `entity` is a
- string indicated the type of object to be retrieved. The id may be
- empty, in which case the query is a search. `includes` is a list
- of strings that must be valid includes for the entity type. `params`
- is a dictionary of additional parameters for the API call. The
- response is parsed and returned.
- """
- # Build arguments.
- if not isinstance(includes, list):
- includes = [includes]
- _check_includes(entity, includes)
- auth_required = _get_auth_type(entity, id, includes)
- args = dict(params)
- if len(includes) > 0:
- inc = " ".join(includes)
- args["inc"] = inc
-
- # Build the endpoint components.
- path = '%s/%s' % (entity, id)
- return _mb_request(path, 'GET', auth_required, args=args)
+ """Make a single GET call to the MusicBrainz XML API. `entity` is a
+ string indicated the type of object to be retrieved. The id may be
+ empty, in which case the query is a search. `includes` is a list
+ of strings that must be valid includes for the entity type. `params`
+ is a dictionary of additional parameters for the API call. The
+ response is parsed and returned.
+ """
+ # Build arguments.
+ if not isinstance(includes, list):
+ includes = [includes]
+ _check_includes(entity, includes)
+ auth_required = _get_auth_type(entity, id, includes)
+ args = dict(params)
+ if len(includes) > 0:
+ inc = " ".join(includes)
+ args["inc"] = inc
+
+ # Build the endpoint components.
+ path = '%s/%s' % (entity, id)
+ return _mb_request(path, 'GET', auth_required, args=args)
def _do_mb_search(entity, query='', fields={},
- limit=None, offset=None, strict=False):
- """Perform a full-text search on the MusicBrainz search server.
- `query` is a lucene query string when no fields are set,
- but is escaped when any fields are given. `fields` is a dictionary
- of key/value query parameters. They keys in `fields` must be valid
- for the given entity type.
- """
- # Encode the query terms as a Lucene query string.
- query_parts = []
- if query:
- clean_query = util._unicode(query)
- if fields:
- clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
- clean_query)
- if strict:
- query_parts.append('"%s"' % clean_query)
- else:
- query_parts.append(clean_query.lower())
- else:
- query_parts.append(clean_query)
- for key, value in fields.items():
- # Ensure this is a valid search field.
- if key not in VALID_SEARCH_FIELDS[entity]:
- raise InvalidSearchFieldError(
- '%s is not a valid search field for %s' % (key, entity)
- )
- elif key == "puid":
- warn("PUID support was removed from server\n"
- "the 'puid' field is ignored",
- Warning, stacklevel=2)
-
- # Escape Lucene's special characters.
- value = util._unicode(value)
- value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
- if value:
- if strict:
- query_parts.append('%s:"%s"' % (key, value))
- else:
- value = value.lower() # avoid AND / OR
- query_parts.append('%s:(%s)' % (key, value))
- if strict:
- full_query = ' AND '.join(query_parts).strip()
- else:
- full_query = ' '.join(query_parts).strip()
-
- if not full_query:
- raise ValueError('at least one query term is required')
-
- # Additional parameters to the search.
- params = {'query': full_query}
- if limit:
- params['limit'] = str(limit)
- if offset:
- params['offset'] = str(offset)
-
- return _do_mb_query(entity, '', [], params)
+ limit=None, offset=None, strict=False):
+ """Perform a full-text search on the MusicBrainz search server.
+ `query` is a lucene query string when no fields are set,
+ but is escaped when any fields are given. `fields` is a dictionary
+ of key/value query parameters. They keys in `fields` must be valid
+ for the given entity type.
+ """
+ # Encode the query terms as a Lucene query string.
+ query_parts = []
+ if query:
+ clean_query = util._unicode(query)
+ if fields:
+ clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
+ clean_query)
+ if strict:
+ query_parts.append('"%s"' % clean_query)
+ else:
+ query_parts.append(clean_query.lower())
+ else:
+ query_parts.append(clean_query)
+ for key, value in fields.items():
+ # Ensure this is a valid search field.
+ if key not in VALID_SEARCH_FIELDS[entity]:
+ raise InvalidSearchFieldError(
+ '%s is not a valid search field for %s' % (key, entity)
+ )
+ elif key == "puid":
+ warn("PUID support was removed from server\n"
+ "the 'puid' field is ignored",
+ Warning, stacklevel=2)
+
+ # Escape Lucene's special characters.
+ value = util._unicode(value)
+ value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
+ if value:
+ if strict:
+ query_parts.append('%s:"%s"' % (key, value))
+ else:
+ value = value.lower() # avoid AND / OR
+ query_parts.append('%s:(%s)' % (key, value))
+ if strict:
+ full_query = ' AND '.join(query_parts).strip()
+ else:
+ full_query = ' '.join(query_parts).strip()
+
+ if not full_query:
+ raise ValueError('at least one query term is required')
+
+ # Additional parameters to the search.
+ params = {'query': full_query}
+ if limit:
+ params['limit'] = str(limit)
+ if offset:
+ params['offset'] = str(offset)
+
+ return _do_mb_query(entity, '', [], params)
def _do_mb_delete(path):
- """Send a DELETE request for the specified object.
- """
- return _mb_request(path, 'DELETE', AUTH_YES, True)
+ """Send a DELETE request for the specified object.
+ """
+ return _mb_request(path, 'DELETE', AUTH_YES, True)
def _do_mb_put(path):
- """Send a PUT request for the specified object.
- """
- return _mb_request(path, 'PUT', AUTH_YES, True)
+ """Send a PUT request for the specified object.
+ """
+ return _mb_request(path, 'PUT', AUTH_YES, True)
def _do_mb_post(path, body):
- """Perform a single POST call for an endpoint with a specified
- request body.
- """
- return _mb_request(path, 'POST', AUTH_YES, True, body=body)
+ """Perform a single POST call for an endpoint with a specified
+ request body.
+ """
+ return _mb_request(path, 'POST', AUTH_YES, True, body=body)
# The main interface!
@@ -993,7 +1002,7 @@ def search_releases(query='', limit=None, offset=None, strict=False, **fields):
@_docstring_search("release-group")
def search_release_groups(query='', limit=None, offset=None,
- strict=False, **fields):
+ strict=False, **fields):
"""Search for release groups and return a dict
with a 'release-group-list' key.