Skip to content

Commit 6e10c1d

Browse files
github-actions[bot]MikeSiLVO
authored andcommitted
[metadata.musicvideos.python] v1.0.0
1 parent 4b5d8a9 commit 6e10c1d

12 files changed

Lines changed: 1251 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<addon id="metadata.musicvideos.python"
3+
name="Music Video Scraper"
4+
version="1.0.0"
5+
provider-name="Team Kodi">
6+
<requires>
7+
<import addon="xbmc.python" version="3.0.0"/>
8+
<import addon="xbmc.metadata" version="2.1.0"/>
9+
</requires>
10+
<extension point="xbmc.metadata.scraper.musicvideos"
11+
library="main.py"
12+
cachepersistence="00:15"/>
13+
<extension point="xbmc.addon.metadata">
14+
<reuselanguageinvoker>true</reuselanguageinvoker>
15+
<platform>all</platform>
16+
<license>GPL-3.0-or-later</license>
17+
<summary lang="en_GB">Multi-source music video scraper</summary>
18+
<description lang="en_GB">Music video metadata from TheAudioDB, Last.fm, Wikipedia, and Fanart.tv.</description>
19+
<news>v1.0.0 - Initial release</news>
20+
</extension>
21+
</addon>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
"""Fanart.tv API client."""
4+
5+
import json
6+
from urllib.request import Request, urlopen
7+
8+
from lib import log
9+
from lib.config import FANARTTV_BASE, FANARTTV_KEY
10+
11+
_ARTIST_MAPPING = {
12+
'artistbackground': 'fanart',
13+
'artist4kbackground': 'fanart',
14+
'artistthumb': 'thumb',
15+
'hdmusiclogo': 'clearlogo',
16+
'musiclogo': 'clearlogo',
17+
'musicbanner': 'banner',
18+
}
19+
20+
_cache = {}
21+
22+
23+
def get_artist_artwork(mbid, settings):
24+
"""Fetch artist artwork by MusicBrainz ID."""
25+
if not settings.get('enable_fanarttv') or not mbid:
26+
return {}
27+
28+
cached = _cache.get(mbid)
29+
if cached is not None:
30+
return cached
31+
32+
data = _fetch(mbid, settings.get('fanarttv_clientkey', ''))
33+
if not data:
34+
_cache[mbid] = {}
35+
return {}
36+
37+
result = {}
38+
for fanart_type, art_type in _ARTIST_MAPPING.items():
39+
items = data.get(fanart_type)
40+
if not items:
41+
continue
42+
for item in items:
43+
url = item.get('url', '')
44+
if not url:
45+
continue
46+
preview = url.replace('/fanart/', '/preview/')
47+
try:
48+
likes = int(item.get('likes') or 0)
49+
except (ValueError, TypeError):
50+
likes = 0
51+
result.setdefault(art_type, []).append((url, preview, likes))
52+
53+
_cache[mbid] = result
54+
return result
55+
56+
57+
def _fetch(mbid, client_key):
58+
"""Make a GET request to the API."""
59+
url = '{}/music/{}'.format(FANARTTV_BASE, mbid)
60+
log.debug('Fanart.tv GET /music/{}'.format(mbid))
61+
headers = {
62+
'api-key': FANARTTV_KEY,
63+
'Accept': 'application/json',
64+
'User-Agent': 'metadata.musicvideos.python3',
65+
}
66+
if client_key:
67+
headers['client-key'] = client_key
68+
try:
69+
req = Request(url, headers=headers)
70+
with urlopen(req, timeout=10) as resp:
71+
return json.loads(resp.read().decode('utf-8'))
72+
except Exception as exc:
73+
log.error('Fanart.tv GET /music/{} failed: {}'.format(mbid, exc))
74+
return None

0 commit comments

Comments
 (0)