From a80f20a5a627855a346998deb49385fa12bb2359 Mon Sep 17 00:00:00 2001 From: L2501 Date: Sat, 27 Apr 2019 23:23:17 +0300 Subject: [PATCH] v3 --- addon.xml | 4 +- default.py | 126 +++++++--- resources/lib/musicmp3.py | 504 +++++++++++++++++++------------------- 3 files changed, 354 insertions(+), 280 deletions(-) diff --git a/addon.xml b/addon.xml index bdae4c3..9eff6e4 100644 --- a/addon.xml +++ b/addon.xml @@ -1,17 +1,17 @@ - + - audio + true all GPL-2.0 A music streaming collection diff --git a/default.py b/default.py index 06c88b0..ba565f2 100644 --- a/default.py +++ b/default.py @@ -1,11 +1,12 @@ import os +import sys import routing import xbmcaddon import xbmc from xbmc import executebuiltin from xbmcgui import ListItem from xbmcplugin import addDirectoryItems, addDirectoryItem, endOfDirectory, setResolvedUrl, setContent -from resources.lib.musicmp3 import musicMp3 +from resources.lib.musicmp3 import musicMp3, gnr_ids try: @@ -29,7 +30,9 @@ def unquote(s): plugin.name = addon.getAddonInfo("name") USER_DATA_DIR = xbmc.translatePath(addon.getAddonInfo("profile")).decode("utf-8") # !! -MEDIA_DIR = os.path.join(xbmc.translatePath(xbmcaddon.Addon().getAddonInfo("path")).decode("utf-8"), "resources", "media") +MEDIA_DIR = os.path.join( + xbmc.translatePath(xbmcaddon.Addon().getAddonInfo("path")).decode("utf-8"), "resources", "media" +) FANART = os.path.join(MEDIA_DIR, "fanart.jpg") MUSICMP3_DIR = os.path.join(USER_DATA_DIR, "musicmp3") if not os.path.exists(MUSICMP3_DIR): @@ -39,8 +42,6 @@ def unquote(s): albums_view_mode = addon.getSetting("albums_view_mode") songs_view_mode = addon.getSetting("songs_view_mode") -musicmp3_api = musicMp3(MUSICMP3_DIR) - @plugin.route("/") def index(): @@ -64,16 +65,27 @@ def index(): @plugin.route("/musicmp3/albums_main/") def musicmp3_albums_main(sort): - for i, gnr in enumerate(musicmp3_api.gnr_ids): + _directory_items = [] + for i, gnr in enumerate(gnr_ids): li = ListItem("{0} {1} Albums".format(sort.title(), gnr[0])) - li.setArt({"fanart": FANART, "icon": os.path.join(MEDIA_DIR, "genre", "{0}.jpg".format(gnr[0].lower().replace(" ", "").replace("&", "_")))}) - addDirectoryItem(plugin.handle, plugin.url_for(musicmp3_albums_gnr, sort, i), li, True) + li.setArt( + { + "fanart": FANART, + "icon": os.path.join( + MEDIA_DIR, "genre", "{0}.jpg".format(gnr[0].lower().replace(" ", "").replace("&", "_")) + ), + } + ) + _directory_items.append((plugin.url_for(musicmp3_albums_gnr, sort, i), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) endOfDirectory(plugin.handle) @plugin.route("/musicmp3/albums_gnr//") def musicmp3_albums_gnr(sort, gnr): - for sub_gnr in musicmp3_api.gnr_ids[int(gnr)][1]: + _directory_items = [] + for sub_gnr in gnr_ids[int(gnr)][1]: if sub_gnr[0] == "Compilations": section = "compilations" elif sub_gnr[0] == "Soundtracks": @@ -86,43 +98,61 @@ def musicmp3_albums_gnr(sort, gnr): { "fanart": FANART, "icon": os.path.join( - MEDIA_DIR, "genre", musicmp3_api.gnr_ids[int(gnr)][0].lower(), "{0}.jpg".format(sub_gnr[0].lower().replace(" ", "").replace("&", "_")) + MEDIA_DIR, + "genre", + gnr_ids[int(gnr)][0].lower(), + "{0}.jpg".format(sub_gnr[0].lower().replace(" ", "").replace("&", "_")), ), } ) - addDirectoryItem(plugin.handle, plugin.url_for(musicmp3_main_albums, section, sub_gnr[1], sort, "0"), li, True) + _directory_items.append((plugin.url_for(musicmp3_main_albums, section, sub_gnr[1], sort, "0"), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) endOfDirectory(plugin.handle) @plugin.route("/musicmp3/search/") def musicmp3_search(cat): + musicmp3_api = musicMp3(MUSICMP3_DIR) keyboard = xbmc.Keyboard("", "Search") keyboard.doModal() if keyboard.isConfirmed(): keyboardinput = keyboard.getText() if keyboardinput: + _directory_items = [] if cat == "artists": artists = musicmp3_api.search(keyboardinput, cat) for a in artists: li = ListItem(a.get("artist")) li.setInfo("music", {"title": a.get("artist"), "artist": a.get("artist")}) - addDirectoryItem(plugin.handle, plugin.url_for(artists_albums, quote(a.get("link"))), li, True) + _directory_items.append((plugin.url_for(artists_albums, quote(a.get("link"))), li, True)) elif cat == "albums": albums = musicmp3_api.search(keyboardinput, cat) for a in albums: li = ListItem("{0}[CR][COLOR=darkmagenta]{1}[/COLOR]".format(a.get("title"), a.get("artist"))) li.setArt({"thumb": a.get("image"), "icon": a.get("image")}) - li.setInfo("music", {"title": a.get("title"), "artist": a.get("artist"), "album": a.get("title"), "year": a.get("date")}) + li.setInfo( + "music", + { + "title": a.get("title"), + "artist": a.get("artist"), + "album": a.get("title"), + "year": a.get("date"), + }, + ) li.setProperty("Album_Description", a.get("details")) - addDirectoryItem(plugin.handle, plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True) + _directory_items.append((plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) setContent(plugin.handle, "albums") if fixed_view_mode: executebuiltin("Container.SetViewMode({0})".format(albums_view_mode)) endOfDirectory(plugin.handle) -@plugin.route("/musicmp3/main_albums/
///") +@plugin.route("/musicmp3/main_albums/
////") def musicmp3_main_albums(section, gnr_id, sort, index): + musicmp3_api = musicMp3(MUSICMP3_DIR) dir_items = 40 index = int(index) if section == "main": @@ -132,45 +162,62 @@ def musicmp3_main_albums(section, gnr_id, sort, index): albums = musicmp3_api.main_albums(_section, gnr_id, sort, index, dir_items) context_menu = [] - next_index = str(index + dir_items) - next_page = "ActivateWindow(Music,{0})".format(plugin.url_for(musicmp3_main_albums, section, gnr_id, sort, next_index)) - context_menu.append(("Next {0}+".format(next_index), next_page)) + if len(albums) >= dir_items: + next_index = str(index + dir_items) + next_page = "ActivateWindow(Music,{0})".format( + plugin.url_for(musicmp3_main_albums, section, gnr_id, sort, next_index) + ) + context_menu.append(("Next {0}+".format(next_index), next_page)) previous_index = str(index - dir_items) if int(previous_index) >= 0: - previous_page = "ActivateWindow(Music,{0})".format(plugin.url_for(musicmp3_main_albums, section, gnr_id, sort, previous_index)) + previous_page = "ActivateWindow(Music,{0})".format( + plugin.url_for(musicmp3_main_albums, section, gnr_id, sort, previous_index) + ) context_menu.append(("Previous {0}+".format(previous_index), previous_page)) + _directory_items = [] for a in albums: li = ListItem("{0}[CR][COLOR=darkmagenta]{1}[/COLOR]".format(a.get("title"), a.get("artist"))) li.setArt({"thumb": a.get("image"), "icon": a.get("image")}) li.addContextMenuItems(context_menu) - li.setInfo("music", {"title": a.get("title"), "artist": a.get("artist"), "album": a.get("title"), "year": a.get("date")}) - addDirectoryItem(plugin.handle, plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True) + li.setInfo( + "music", + {"title": a.get("title"), "artist": a.get("artist"), "album": a.get("title"), "year": a.get("date")}, + ) + _directory_items.append((plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) setContent(plugin.handle, "albums") if fixed_view_mode: executebuiltin("Container.SetViewMode({0})".format(albums_view_mode)) endOfDirectory(plugin.handle) -@plugin.route("/musicmp3/main_artists/") +@plugin.route("/musicmp3/main_artists//") def musicmp3_main_artists(index): + musicmp3_api = musicMp3(MUSICMP3_DIR) dir_items = 40 index = int(index) artists = musicmp3_api.main_artists(index, dir_items) context_menu = [] - next_index = str(index + dir_items) - next_page = "ActivateWindow(Music,{0})".format(plugin.url_for(musicmp3_main_artists, next_index)) - context_menu.append(("Next {0}+".format(next_index), next_page)) + if len(artists) >= dir_items: + next_index = str(index + dir_items) + next_page = "ActivateWindow(Music,{0})".format(plugin.url_for(musicmp3_main_artists, next_index)) + context_menu.append(("Next {0}+".format(next_index), next_page)) previous_index = str(index - dir_items) if int(previous_index) >= 0: previous_page = "ActivateWindow(Music,{0})".format(plugin.url_for(musicmp3_main_artists, previous_index)) context_menu.append(("Previous {0}+".format(previous_index), previous_page)) + _directory_items = [] for a in artists: li = ListItem(a.get("artist")) li.setInfo("music", {"title": a.get("artist"), "artist": a.get("artist")}) - addDirectoryItem(plugin.handle, plugin.url_for(artists_albums, quote(a.get("link"))), li, True) + li.addContextMenuItems(context_menu) + _directory_items.append((plugin.url_for(artists_albums, quote(a.get("link"))), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) setContent(plugin.handle, "albums") if fixed_view_mode: executebuiltin("Container.SetViewMode({0})".format(albums_view_mode)) @@ -179,14 +226,22 @@ def musicmp3_main_artists(index): @plugin.route("/musicmp3/artists_albums/") def artists_albums(link): + musicmp3_api = musicMp3(MUSICMP3_DIR) url = unquote(link) albums = musicmp3_api.artist_albums(url) + + _directory_items = [] for a in albums: li = ListItem("{0}[CR][COLOR=darkmagenta]{1}[/COLOR]".format(a.get("title"), a.get("artist"))) li.setArt({"thumb": a.get("image"), "icon": a.get("image")}) - li.setInfo("music", {"title": a.get("title"), "artist": a.get("artist"), "album": a.get("title"), "year": a.get("date")}) + li.setInfo( + "music", + {"title": a.get("title"), "artist": a.get("artist"), "album": a.get("title"), "year": a.get("date")}, + ) li.setProperty("Album_Description", a.get("details")) - addDirectoryItem(plugin.handle, plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True) + _directory_items.append((plugin.url_for(musicmp3_album, quote(a.get("link"))), li, True)) + + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) setContent(plugin.handle, "albums") if fixed_view_mode: executebuiltin("Container.SetViewMode({0})".format(albums_view_mode)) @@ -195,18 +250,26 @@ def artists_albums(link): @plugin.route("/musicmp3/album/") def musicmp3_album(link): + musicmp3_api = musicMp3(MUSICMP3_DIR) url = unquote(link) tracks = musicmp3_api.album_tracks(url) _directory_items = [] for t in tracks: - _infolabels = {"title": t.get("title"), "artist": t.get("artist"), "album": t.get("album"), "duration": t.get("duration")} + _infolabels = { + "title": t.get("title"), + "artist": t.get("artist"), + "album": t.get("album"), + "duration": t.get("duration"), + } li = ListItem(t.get("title")) li.setProperty("IsPlayable", "true") li.setArt({"thumb": t.get("image"), "icon": t.get("image")}) li.setInfo("music", _infolabels) - _directory_items.append((plugin.url_for(musicmp3_play, track_id=t.get("track_id"), rel=t.get("rel")), li, False)) + _directory_items.append( + (plugin.url_for(musicmp3_play, track_id=t.get("track_id"), rel=t.get("rel")), li, False) + ) - addDirectoryItems(plugin.handle, _directory_items) + addDirectoryItems(plugin.handle, _directory_items, len(_directory_items)) setContent(plugin.handle, "songs") if fixed_view_mode: executebuiltin("Container.SetViewMode({0})".format(songs_view_mode)) @@ -215,6 +278,7 @@ def musicmp3_album(link): @plugin.route("/musicmp3/play//") def musicmp3_play(track_id, rel): + musicmp3_api = musicMp3(MUSICMP3_DIR) _track = musicmp3_api.get_track(rel) _infolabels = {"title": _track.title, "artist": _track.artist, "album": _track.album, "duration": _track.duration} li = ListItem(_track.title, path=musicmp3_api.play_url(track_id, rel)) @@ -226,4 +290,4 @@ def musicmp3_play(track_id, rel): if __name__ == "__main__": - plugin.run() + plugin.run(sys.argv) diff --git a/resources/lib/musicmp3.py b/resources/lib/musicmp3.py index 4623cbf..6075327 100644 --- a/resources/lib/musicmp3.py +++ b/resources/lib/musicmp3.py @@ -1,7 +1,6 @@ import os import isodate import requests -import requests_cache from bs4 import BeautifulSoup from datetime import timedelta, datetime from urlparse import urljoin @@ -50,9 +49,7 @@ def __init__(self, cache_dir): db.create_tables([Track], safe=True) self.base_url = "https://musicmp3.ru/" self.user_agent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:66.0) Gecko/20100101 Firefox/66.0" - self.s = requests_cache.CachedSession( - os.path.join(cache_dir, "cache"), allowable_methods="GET", expire_after=timedelta(hours=40), old_data_on_error=True - ) + self.s = requests.Session() self.s.cookies = LWPCookieJar(filename=COOKIE_FILE) self.s.headers.update({"User-Agent": self.user_agent}) if os.path.isfile(COOKIE_FILE): @@ -60,7 +57,6 @@ def __init__(self, cache_dir): def __del__(self): db.close() - self.s.cache.remove_old_entries(datetime.utcnow() - timedelta(hours=40)) self.s.cookies.save(ignore_discard=True, ignore_expires=True) self.s.close() @@ -121,7 +117,9 @@ def search(self, text, cat): _list = [] if cat == "artists": for artist in soup.find_all(class_="artist_preview"): - _list.append({"artist": artist.a.get_text(strip=True), "link": urljoin(self.base_url, artist.a.get("href"))}) + _list.append( + {"artist": artist.a.get_text(strip=True), "link": urljoin(self.base_url, artist.a.get("href"))} + ) elif cat == "albums": for album in soup.find_all(class_="album_report"): album_report = { @@ -142,7 +140,9 @@ def main_artists(self, start, count): _list = [] while len(_list) < count: params = {"type": "artist", "page": _page} - r = self.s.get("https://musicmp3.ru/main_artists.html", params=params, headers={"Referer": self.base_url}, timeout=5) + r = self.s.get( + "https://musicmp3.ru/main_artists.html", params=params, headers={"Referer": self.base_url}, timeout=5 + ) soup = BeautifulSoup(r.text, "html.parser") if not soup.a: break @@ -207,7 +207,9 @@ def album_tracks(self, url): track["title"] = song.find(itemprop="name").get_text(strip=True) track["artist"] = song.find(itemprop="byArtist").get("content") track["album"] = song.find(itemprop="inAlbum").get("content") - track["duration"] = str(isodate.parse_duration(song.find(itemprop="duration").get("content")).total_seconds()) + track["duration"] = str( + isodate.parse_duration(song.find(itemprop="duration").get("content")).total_seconds() + ) track["image"] = self.image_url(image) track["track_id"] = song.get("id") track["rel"] = song.a.get("rel")[0] @@ -223,242 +225,250 @@ def get_track(self, rel): except: return Track() - gnr_ids = [ - ( - "World", - [ - ("World", "0"), - ("Celtic", "3"), - ("Jewish", "14"), - ("Polynesian", "20"), - ("African", "23"), - ("Arabic", "79"), - ("Brazilian", "93"), - ("Caribbean", "135"), - ("Turkish", "164"), - ("Chinese", "169"), - ("Japanese", "179"), - ("Korean", "194"), - ("South Asian", "200"), - ("Spanish Folk", "212"), - ("South American Folk", "220"), - ("Slavic Folk", "229"), - ("Nordic Folk", "241"), - ("Italian Folk", "249"), - ("French Folk", "252"), - ("Balkan Folk", "259"), - ("Latin", "268"), - ("Compilations", "2"), - ], - ), - ( - "Classical", - [ - ("Classical", "313"), - ("Baroque Period", "314"), - ("Chamber", "315"), - ("Choral", "316"), - ("Classical Period", "317"), - ("Medieval", "318"), - ("Modern Classical", "326"), - ("Opera", "343"), - ("Orchestral", "348"), - ("Renaissance", "352"), - ("Romantic Period", "353"), - ("Classical Crossover", "354"), - ("Compilations", "313"), - ], - ), - ( - "Metal", - [ - ("Metal", "355"), - ("Alternative Metal", "356"), - ("Black Metal", "360"), - ("Death Metal", "365"), - ("Doom Metal", "373"), - ("Folk Metal", "378"), - ("Gothic Metal", "382"), - ("Grindcore", "383"), - ("Groove Metal", "386"), - ("Heavy Metal", "387"), - ("Industrial Metal", "389"), - ("Metalcore", "391"), - ("Neo-Classical Metal", "395"), - ("Power Metal", "396"), - ("Progressive Metal", "397"), - ("Symphonic Metal", "398"), - ("Thrash & Speed Metal", "399"), - ("Sludge Metal", "404"), - ("Glam Metal", "407"), - ("Compilations", "355"), - ], - ), - ( - "Alternative", - [ - ("Alternative", "408"), - ("Britpop", "409"), - ("Dream Pop", "410"), - ("Grunge", "412"), - ("Indie Rock", "414"), - ("Industrial Rock", "419"), - ("Rap Rock", "420"), - ("Garage Rock", "421"), - ("Latin Alternative", "286"), - ("Post-Punk", "424"), - ("Emo", "431"), - ("Punk Rock", "436"), - ("Compilations", "408"), - ], - ), - ( - "Rock", - [ - ("Rock", "473"), - ("Art Rock", "474"), - ("Christian Rock", "481"), - ("Comedy Rock", "482"), - ("Folk Rock", "483"), - ("Glam Rock", "489"), - ("Hard Rock", "491"), - ("Latin Rock", "292"), - ("Progressive Rock", "494"), - ("Psychedelic Rock", "500"), - ("Rock & Roll", "507"), - ("Southern Rock", "515"), - ("Rockabilly", "516"), - ("Compilations", "473"), - ], - ), - ( - "R&B", - [ - ("R&B", "517"), - ("Contemporary R&B", "518"), - ("Funk", "520"), - ("Soul", "525"), - ("Early R&B", "534"), - ("Pop Soul", "537"), - ("Neo-Soul", "538"), - ("Compilations", "517"), - ], - ), - ( - "Dance", - [ - ("Dance", "539"), - ("Teen Pop", "540"), - ("Hi-NRG", "542"), - ("Dance Pop", "543"), - ("Electropop", "547"), - ("Alternative Dance", "549"), - ("Disco", "551"), - ("Eurodance", "557"), - ("Compilations", "539"), - ], - ), - ( - "Pop", - [ - ("Pop", "558"), - ("Adult Contemporary", "559"), - ("CCM", "560"), - ("Euro Pop", "562"), - ("French Pop", "564"), - ("Indie Pop", "567"), - ("Latin Pop", "291"), - ("Pop Rock", "571"), - ("Traditional Pop", "579"), - ("New Wave", "582"), - ("Easy Listening", "589"), - ("Blue Eyed Soul", "595"), - ("Compilations", "558"), - ], - ), - ( - "Jazz", - [ - ("Jazz", "596"), - ("Acid Jazz", "597"), - ("Free Jazz", "599"), - ("Bebop", "600"), - ("Big Band", "603"), - ("Cool Jazz", "606"), - ("Jazz Fusion", "607"), - ("Soul Jazz", "610"), - ("Swing", "611"), - ("Vocal Jazz", "613"), - ("Early Jazz", "614"), - ("World Jazz", "622"), - ("Compilations", "596"), - ], - ), - ( - "Hip Hop", - [ - ("Hip Hop", "623"), - ("Alternative Hip Hop", "624"), - ("Comedy Rap", "629"), - ("East Coast Hip Hop", "630"), - ("French Hip Hop", "631"), - ("Hardcore Hip Hop", "632"), - ("Instrumental Hip Hop", "637"), - ("Political Hip Hop", "638"), - ("Pop Rap", "639"), - ("Religious Hip Hop", "640"), - ("Southern Hip Hop", "644"), - ("UK Hip Hop", "652"), - ("West Coast Hip Hop", "653"), - ("Compilations", "623"), - ], - ), - ( - "Electronic", - [ - ("Electronic", "654"), - ("Breakbeat", "655"), - ("Downtempo", "661"), - ("Drum and Bass", "664"), - ("EBM", "678"), - ("Electro", "681"), - ("Hardcore Techno", "686"), - ("House", "698"), - ("IDM", "717"), - ("Indie Electronic", "718"), - ("Techno", "720"), - ("Trance", "728"), - ("UK Garage", "737"), - ("Ambient", "744"), - ("Dubstep", "749"), - ("Compilations", "654"), - ], - ), - ( - "Country", - [ - ("Country", "750"), - ("Alternative Country", "751"), - ("Contemporary Country", "755"), - ("Country Pop", "756"), - ("Traditional Country", "759"), - ("Country Rock", "770"), - ("Compilations", "750"), - ], - ), - ( - "Blues", - [("Blues", "774"), ("Acoustic Blues", "775"), ("Electric Blues", "780"), ("Piano Blues", "784"), ("Blues Rock", "786"), ("Compilations", "774")], - ), - ( - "Soundtracks", - [ - ("Soundtracks", "0"), - ("Movie Soundtracks", "789"), - ("TV Soundtracks", "792"), - ("Game Soundtracks", "794"), - ("Show Tunes", "796"), - ("Spoken Word", "797"), - ], - ), - ] + +gnr_ids = [ + ( + "World", + [ + ("World", "0"), + ("Celtic", "3"), + ("Jewish", "14"), + ("Polynesian", "20"), + ("African", "23"), + ("Arabic", "79"), + ("Brazilian", "93"), + ("Caribbean", "135"), + ("Turkish", "164"), + ("Chinese", "169"), + ("Japanese", "179"), + ("Korean", "194"), + ("South Asian", "200"), + ("Spanish Folk", "212"), + ("South American Folk", "220"), + ("Slavic Folk", "229"), + ("Nordic Folk", "241"), + ("Italian Folk", "249"), + ("French Folk", "252"), + ("Balkan Folk", "259"), + ("Latin", "268"), + ("Compilations", "2"), + ], + ), + ( + "Classical", + [ + ("Classical", "313"), + ("Baroque Period", "314"), + ("Chamber", "315"), + ("Choral", "316"), + ("Classical Period", "317"), + ("Medieval", "318"), + ("Modern Classical", "326"), + ("Opera", "343"), + ("Orchestral", "348"), + ("Renaissance", "352"), + ("Romantic Period", "353"), + ("Classical Crossover", "354"), + ("Compilations", "313"), + ], + ), + ( + "Metal", + [ + ("Metal", "355"), + ("Alternative Metal", "356"), + ("Black Metal", "360"), + ("Death Metal", "365"), + ("Doom Metal", "373"), + ("Folk Metal", "378"), + ("Gothic Metal", "382"), + ("Grindcore", "383"), + ("Groove Metal", "386"), + ("Heavy Metal", "387"), + ("Industrial Metal", "389"), + ("Metalcore", "391"), + ("Neo-Classical Metal", "395"), + ("Power Metal", "396"), + ("Progressive Metal", "397"), + ("Symphonic Metal", "398"), + ("Thrash & Speed Metal", "399"), + ("Sludge Metal", "404"), + ("Glam Metal", "407"), + ("Compilations", "355"), + ], + ), + ( + "Alternative", + [ + ("Alternative", "408"), + ("Britpop", "409"), + ("Dream Pop", "410"), + ("Grunge", "412"), + ("Indie Rock", "414"), + ("Industrial Rock", "419"), + ("Rap Rock", "420"), + ("Garage Rock", "421"), + ("Latin Alternative", "286"), + ("Post-Punk", "424"), + ("Emo", "431"), + ("Punk Rock", "436"), + ("Compilations", "408"), + ], + ), + ( + "Rock", + [ + ("Rock", "473"), + ("Art Rock", "474"), + ("Christian Rock", "481"), + ("Comedy Rock", "482"), + ("Folk Rock", "483"), + ("Glam Rock", "489"), + ("Hard Rock", "491"), + ("Latin Rock", "292"), + ("Progressive Rock", "494"), + ("Psychedelic Rock", "500"), + ("Rock & Roll", "507"), + ("Southern Rock", "515"), + ("Rockabilly", "516"), + ("Compilations", "473"), + ], + ), + ( + "R&B", + [ + ("R&B", "517"), + ("Contemporary R&B", "518"), + ("Funk", "520"), + ("Soul", "525"), + ("Early R&B", "534"), + ("Pop Soul", "537"), + ("Neo-Soul", "538"), + ("Compilations", "517"), + ], + ), + ( + "Dance", + [ + ("Dance", "539"), + ("Teen Pop", "540"), + ("Hi-NRG", "542"), + ("Dance Pop", "543"), + ("Electropop", "547"), + ("Alternative Dance", "549"), + ("Disco", "551"), + ("Eurodance", "557"), + ("Compilations", "539"), + ], + ), + ( + "Pop", + [ + ("Pop", "558"), + ("Adult Contemporary", "559"), + ("CCM", "560"), + ("Euro Pop", "562"), + ("French Pop", "564"), + ("Indie Pop", "567"), + ("Latin Pop", "291"), + ("Pop Rock", "571"), + ("Traditional Pop", "579"), + ("New Wave", "582"), + ("Easy Listening", "589"), + ("Blue Eyed Soul", "595"), + ("Compilations", "558"), + ], + ), + ( + "Jazz", + [ + ("Jazz", "596"), + ("Acid Jazz", "597"), + ("Free Jazz", "599"), + ("Bebop", "600"), + ("Big Band", "603"), + ("Cool Jazz", "606"), + ("Jazz Fusion", "607"), + ("Soul Jazz", "610"), + ("Swing", "611"), + ("Vocal Jazz", "613"), + ("Early Jazz", "614"), + ("World Jazz", "622"), + ("Compilations", "596"), + ], + ), + ( + "Hip Hop", + [ + ("Hip Hop", "623"), + ("Alternative Hip Hop", "624"), + ("Comedy Rap", "629"), + ("East Coast Hip Hop", "630"), + ("French Hip Hop", "631"), + ("Hardcore Hip Hop", "632"), + ("Instrumental Hip Hop", "637"), + ("Political Hip Hop", "638"), + ("Pop Rap", "639"), + ("Religious Hip Hop", "640"), + ("Southern Hip Hop", "644"), + ("UK Hip Hop", "652"), + ("West Coast Hip Hop", "653"), + ("Compilations", "623"), + ], + ), + ( + "Electronic", + [ + ("Electronic", "654"), + ("Breakbeat", "655"), + ("Downtempo", "661"), + ("Drum and Bass", "664"), + ("EBM", "678"), + ("Electro", "681"), + ("Hardcore Techno", "686"), + ("House", "698"), + ("IDM", "717"), + ("Indie Electronic", "718"), + ("Techno", "720"), + ("Trance", "728"), + ("UK Garage", "737"), + ("Ambient", "744"), + ("Dubstep", "749"), + ("Compilations", "654"), + ], + ), + ( + "Country", + [ + ("Country", "750"), + ("Alternative Country", "751"), + ("Contemporary Country", "755"), + ("Country Pop", "756"), + ("Traditional Country", "759"), + ("Country Rock", "770"), + ("Compilations", "750"), + ], + ), + ( + "Blues", + [ + ("Blues", "774"), + ("Acoustic Blues", "775"), + ("Electric Blues", "780"), + ("Piano Blues", "784"), + ("Blues Rock", "786"), + ("Compilations", "774"), + ], + ), + ( + "Soundtracks", + [ + ("Soundtracks", "0"), + ("Movie Soundtracks", "789"), + ("TV Soundtracks", "792"), + ("Game Soundtracks", "794"), + ("Show Tunes", "796"), + ("Spoken Word", "797"), + ], + ), +]