|
| 1 | +# SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | + |
| 3 | +"""TheAudioDB API client.""" |
| 4 | + |
| 5 | +import json |
| 6 | +from urllib.request import Request, urlopen |
| 7 | +from urllib.parse import quote |
| 8 | + |
| 9 | +from lib import log |
| 10 | +from lib.config import AUDIODB_BASE |
| 11 | + |
| 12 | +_track_cache = {} |
| 13 | +_artist_cache = {} |
| 14 | +_album_cache = {} |
| 15 | + |
| 16 | + |
| 17 | +def normalize_url(url): |
| 18 | + """Normalize an image URL from the API response.""" |
| 19 | + return url or '' |
| 20 | + |
| 21 | + |
| 22 | +def normalize_quotes(text): |
| 23 | + """Replace smart quotes with ASCII equivalents for API queries.""" |
| 24 | + if not text: |
| 25 | + return '' |
| 26 | + return (text |
| 27 | + .replace('\u2018', "'").replace('\u2019', "'") |
| 28 | + .replace('\u201c', '"').replace('\u201d', '"')) |
| 29 | + |
| 30 | + |
| 31 | +def search_tracks(artist, track): |
| 32 | + """Search for tracks by artist and title.""" |
| 33 | + artist = normalize_quotes(artist) |
| 34 | + track = normalize_quotes(track) |
| 35 | + data = _get('/searchtrack.php?s={}&t={}'.format( |
| 36 | + quote(artist, safe=''), quote(track, safe=''), |
| 37 | + )) |
| 38 | + if not data: |
| 39 | + return [] |
| 40 | + tracks = data.get('track') |
| 41 | + if not tracks or not isinstance(tracks, list): |
| 42 | + return [] |
| 43 | + for t in tracks: |
| 44 | + tid = t.get('idTrack') |
| 45 | + if tid: |
| 46 | + _track_cache[str(tid)] = t |
| 47 | + return tracks |
| 48 | + |
| 49 | + |
| 50 | +def get_cached_track(track_id): |
| 51 | + """Return a previously fetched track, or None.""" |
| 52 | + return _track_cache.get(str(track_id)) |
| 53 | + |
| 54 | + |
| 55 | +def get_track_by_id(track_id): |
| 56 | + """Fetch a single track by its ID.""" |
| 57 | + track_id = str(track_id) |
| 58 | + cached = _track_cache.get(track_id) |
| 59 | + if cached: |
| 60 | + return cached |
| 61 | + data = _get('/track.php?h={}'.format(track_id)) |
| 62 | + if not data: |
| 63 | + return None |
| 64 | + tracks = data.get('track') |
| 65 | + if not tracks or not isinstance(tracks, list): |
| 66 | + return None |
| 67 | + t = tracks[0] |
| 68 | + _track_cache[track_id] = t |
| 69 | + return t |
| 70 | + |
| 71 | + |
| 72 | +def search_artist(artist_name): |
| 73 | + """Search for an artist by name.""" |
| 74 | + key = artist_name.lower() |
| 75 | + cached = _artist_cache.get(key) |
| 76 | + if cached is not None: |
| 77 | + return cached or None |
| 78 | + |
| 79 | + name = normalize_quotes(artist_name) |
| 80 | + data = _get('/search.php?s={}'.format(quote(name, safe=''))) |
| 81 | + if not data: |
| 82 | + _artist_cache[key] = {} |
| 83 | + return None |
| 84 | + artists = data.get('artists') |
| 85 | + if not artists or not isinstance(artists, list): |
| 86 | + _artist_cache[key] = {} |
| 87 | + return None |
| 88 | + |
| 89 | + for a in artists: |
| 90 | + if a.get('strArtist', '').lower() == key: |
| 91 | + _artist_cache[key] = a |
| 92 | + return a |
| 93 | + |
| 94 | + _artist_cache[key] = artists[0] |
| 95 | + return artists[0] |
| 96 | + |
| 97 | + |
| 98 | +def get_album(album_id): |
| 99 | + """Fetch an album by its ID.""" |
| 100 | + album_id = str(album_id) |
| 101 | + cached = _album_cache.get(album_id) |
| 102 | + if cached is not None: |
| 103 | + return cached or None |
| 104 | + data = _get('/album.php?m={}'.format(album_id)) |
| 105 | + if not data: |
| 106 | + _album_cache[album_id] = {} |
| 107 | + return None |
| 108 | + albums = data.get('album') |
| 109 | + if not albums or not isinstance(albums, list): |
| 110 | + _album_cache[album_id] = {} |
| 111 | + return None |
| 112 | + a = albums[0] |
| 113 | + _album_cache[album_id] = a |
| 114 | + return a |
| 115 | + |
| 116 | + |
| 117 | +def get_track_screenshots(track): |
| 118 | + """Extract music video screenshot URLs from a track.""" |
| 119 | + if not track: |
| 120 | + return [] |
| 121 | + screens = [] |
| 122 | + for suffix in [''] + list(range(2, 13)): |
| 123 | + url = track.get('strMusicVidScreen{}'.format(suffix)) |
| 124 | + if url: |
| 125 | + url = normalize_url(url) |
| 126 | + screens.append((url, '{}/preview'.format(url))) |
| 127 | + return screens |
| 128 | + |
| 129 | + |
| 130 | +_ARTIST_ART_MAPPING = { |
| 131 | + 'strArtistThumb': 'thumb', |
| 132 | + 'strArtistLogo': 'clearlogo', |
| 133 | + 'strArtistBanner': 'banner', |
| 134 | + 'strArtistFanart': 'fanart', |
| 135 | + 'strArtistFanart2': 'fanart', |
| 136 | + 'strArtistFanart3': 'fanart', |
| 137 | + 'strArtistFanart4': 'fanart', |
| 138 | + 'strArtistClearart': 'clearart', |
| 139 | + 'strArtistWideThumb': 'landscape', |
| 140 | +} |
| 141 | + |
| 142 | + |
| 143 | +def get_artist_artwork(artist): |
| 144 | + """Extract artwork URLs from an artist result.""" |
| 145 | + if not artist: |
| 146 | + return {} |
| 147 | + result = {} |
| 148 | + for api_key, art_type in _ARTIST_ART_MAPPING.items(): |
| 149 | + url = artist.get(api_key) |
| 150 | + if url: |
| 151 | + url = normalize_url(url) |
| 152 | + preview = '{}/preview'.format(url) |
| 153 | + result.setdefault(art_type, []).append((url, preview)) |
| 154 | + return result |
| 155 | + |
| 156 | + |
| 157 | +def _get(path): |
| 158 | + """Make a GET request to the API.""" |
| 159 | + url = '{}{}'.format(AUDIODB_BASE, path) |
| 160 | + log.debug('AudioDB GET {}'.format(path.split('?')[0])) |
| 161 | + try: |
| 162 | + req = Request(url, headers={ |
| 163 | + 'Accept': 'application/json', |
| 164 | + 'User-Agent': 'metadata.musicvideos.python3', |
| 165 | + }) |
| 166 | + with urlopen(req, timeout=15) as resp: |
| 167 | + return json.loads(resp.read().decode('utf-8')) |
| 168 | + except Exception as exc: |
| 169 | + log.error('AudioDB GET failed: {}'.format(exc)) |
| 170 | + return None |
0 commit comments