import mimetypes import urllib.request import urllib.error from xml.dom.minidom import Document, Element from flask import Flask, request, abort, Response import musicbrainzngs from musicbrainzngs.musicbrainz import ResponseError import api.discogs import api.deezer import api.imdb import utils from atom.factory import * from locale import getdefaultlocale sys_locale = getdefaultlocale()[0] app = Flask(__name__) musicbrainzngs.set_useragent("Zune", "4.8", "https://github.com/ZuneDev/PyZuneCatalogServer") import re @app.after_request def allow_zunestk_cors(response): r = request.origin if r is None: return response if re.match(r"https?://(127\.0\.0\.(?:\d*)|localhost(?::\d+)?|(?:\w*\.)*zunes\.(tk|me))", r): response.headers.add('Access-Control-Allow-Origin', r) response.headers.add('Access-Control-Allow-Credentials', 'true') response.headers.add('Access-Control-Allow-Headers', 'Content-Type') response.headers.add('Access-Control-Allow-Headers', 'Cache-Control') response.headers.add('Access-Control-Allow-Headers', 'X-Requested-With') response.headers.add('Access-Control-Allow-Headers', 'Authorization') response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE') return response @app.route("/") def default(): return Response("Welcome to the Social", mimetype="text/plain") @app.route(f"/v3.2//hubs/music") def hubs_music(locale: str): with open(f'reference/catalog.zune.net_v3.2_{locale}_hubs_music.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) @app.route(f"/v3.2//music/genre//") def music_genre_details(id: str, locale: str): with open(f'reference/catalog.zune.net_v3.2_{locale}_music_genre.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) @app.route(f"/v3.2//music/genre//albums/") def music_genre_albums(z_genre_id: str, locale: str): from musicbrainz.genrelist import GENRES, get_zune_genre_name z_genre_name: str = get_zune_genre_name(z_genre_id) doc: Document = minidom.Document() feed: Element = create_feed(doc, z_genre_name, z_genre_id, request.endpoint) max_num_albums: int = 50 num_albums: int = 0 done: bool = False for mb_genre_id, mb_genre_name in GENRES[z_genre_name].items(): try: albums = musicbrainzngs.search_releases(mb_genre_name, "tag")["release-list"] except ResponseError as error: abort(error.cause.code) return for album in albums: done = num_albums > max_num_albums if done: break album_name: str = album["title"] album_id: str = album["id"] artist = album["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] entry: Element = create_entry(doc, album_name, album_id, f"/v3.2/{locale}/music/album/{album_id}") # Set primaryArtist primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) num_albums += 1 feed.appendChild(entry) if done: break xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) @app.route(f"/v3.2//music/genre/") def music_genre(locale: str): from musicbrainz.genrelist import GENRES, get_zune_genre_id lastpulledlist: datetime = datetime(2021, 2, 21) doc: Document = minidom.Document() feed: Element = create_feed(doc, "Genres", "genre", request.endpoint, lastpulledlist) for z_genre_name, mb_genres in GENRES.items(): z_genre_id: str = get_zune_genre_id(z_genre_name) feed_link: str = f"/v3.2/{locale}/music/genre/{z_genre_id}" entry: Element = create_entry(doc, z_genre_name, z_genre_id, feed_link, lastpulledlist) feed.appendChild(entry) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) @app.route(f"/v3.2//music/album//") def music_get_album(album_id: str, locale: str): try: album = musicbrainzngs.get_release_by_id(album_id, includes=["artists", "recordings"])["release"] except ResponseError as error: abort(error.cause.code) return album_name: str = album["title"] artist = album["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] tracks = album["medium-list"][0]["track-list"] doc: Document = minidom.Document() feed: Element = create_feed(doc, album_name, album_id, request.endpoint) if bool(album["cover-art-archive"]["front"]): # Add front cover image_elem: Element = doc.createElement("image") image_id_elem: Element = create_id(doc, album_id) image_elem.appendChild(image_id_elem) feed.appendChild(image_elem) for track in tracks: recording = track["recording"] track_id: str = recording["id"] track_title: str = recording["title"] entry: Element = create_entry(doc, track_title, track_id, f"/v3.2/{locale}/") # Set primaryArtist primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) try: length_ms: int = int(track["track_or_recording_length"]) length_elem: Element = doc.createElement("duration") length_timespan: str = utils.format_timespan(length_ms) set_element_value(length_elem, length_timespan) entry.appendChild(length_elem) except: pass try: track_position: str = track["position"] index_elem: Element = doc.createElement("trackNumber") set_element_value(index_elem, track_position) entry.appendChild(index_elem) except: pass try: # Add rights right: Element = doc.createElement("right") right_props = { # "providerName": "YouTube", "providerCode": "117767492:MP3_DOWNLOAD_UENC_256kb_075", "price": 800, "priceCurrencyCode": "MPT", "licenseType": "Preview", "audioEncoding": "MP3", "offerId": "urn:uuid:9534a201-2102-11db-89ca-0019b92a3933", "paymentTypes": { "paymentType": "Microsoft Points" } # "offerInstanceId": "urn:uuid:9534a201-2102-11db-89ca-0019b92a3933", } set_values_as_elements(doc, right, right_props) rights: Element = doc.createElement("rights") rights.appendChild(right) entry.appendChild(rights) set_value_as_element(doc, entry, "isActionable", "True") set_value_as_element(doc, entry, "canPlay", "True") except: pass feed.appendChild(entry) # doc.appendChild(feed) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) @app.route(f"/v3.2//music/album///") def music_album_details(id: str, fragment: str, locale: str): return fragment # Get artist information @app.route(f"/v3.2//music/artist//") def music_get_artist(id: str, locale: str): artist, mb_artist = api.discogs.get_artist_from_mbid(id) artist_name: str = mb_artist["name"] artist_sort_name: str = mb_artist["sort-name"] #if type(artist) is int: # abort(artist) # return doc: Document = minidom.Document() feed: Element = create_feed(doc, artist_name, id, request.endpoint) artist_props = { "id": id, "title": artist_name, "sortTitle": artist_sort_name, "isVariousArtist": False, #"biographyLink": artist["resource_url"], # TODO: How to set artist image? "backgroundImageId": "7da9ffbf-2b5a-4162-b2eb-5818bd6574f3" } set_values_as_elements(doc, feed, artist_props) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) # Get artist's tracks @app.route(f"/v3.2//music/artist//tracks/") def music_get_artist_tracks(artist_id: str, locale: str): try: recordings = musicbrainzngs.browse_recordings(artist_id, limit=100)["recording-list"] except ResponseError as error: abort(error.cause.code) return artist = musicbrainzngs.get_artist_by_id(artist_id)["artist"] artist_name: str = artist["name"] doc: Document = minidom.Document() feed: Element = create_feed(doc, artist_name, artist_id, request.endpoint) for recording in recordings: id: str = recording["id"] title: str = recording["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/artist/{artist_id}/tracks/{id}") feed.appendChild(entry) # Create primaryArtist element primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) # Get artist's albums @app.route(f"/v3.2//music/artist//albums/") def music_get_artist_albums(artist_id: str, locale: str): try: releases = musicbrainzngs.browse_releases(artist_id, limit=100)["release-list"] except ResponseError as error: abort(error.cause.code) return artist = musicbrainzngs.get_artist_by_id(artist_id)["artist"] artist_name: str = artist["name"] doc: Document = minidom.Document() feed: Element = create_feed(doc, artist_name, artist_id, request.endpoint) for release in releases: id: str = release["id"] title: str = release["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/artist/{artist_id}/albums/{id}") feed.appendChild(entry) # Add front cover image_elem: Element = doc.createElement("image") image_id_elem: Element = create_id(doc, id) image_elem.appendChild(image_id_elem) entry.appendChild(image_elem) # Create primaryArtist element primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) #doc.appendChild(feed) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) @app.route(f"/v3.2//music/artist//primaryImage/") def music_artist_primaryImage(id: str, locale: str): response = api.deezer.get_artist_from_mbid(id) artist, mb_artist = response if type(artist) is int: abort(artist) return # Request the image from the API and forward it to the Zune software try: image = urllib.request.urlopen(artist["picture_big"]) return Response(image.read(), mimetype=MIME_JPG) except urllib.error.HTTPError as error: abort(error.code) @app.route("/v3.2//music/track//") def music_get_track(mbid: str, locale: str): try: response = musicbrainzngs.get_recording_by_id(mbid, includes=["artist-credits", "releases"]) except ResponseError as error: abort(error.cause.code) return recording = response["recording"] doc: Document = minidom.Document() # Set track ID and Title id: str = recording["id"] title: str = recording["title"] feed: Element = create_feed(doc, title, id, request.endpoint) entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/track/" + id) feed.appendChild(entry) # Get artist ID and Name artist = recording["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] # Create primaryArtist element primary_artist_elem: Element = doc.createElement("primaryArtist") artist_id_element: Element = doc.createElement("id") set_element_value(artist_id_element, artist_id) primary_artist_elem.appendChild(artist_id_element) artist_name_element: Element = doc.createElement("name") set_element_value(artist_name_element, artist_name) primary_artist_elem.appendChild(artist_name_element) entry.appendChild(primary_artist_elem) # Get album ID and Title album = recording["release-list"][0] album_id: str = album["id"] album_name: str = album["title"] # Create album elements album_id_element: Element = doc.createElement("albumId") set_element_value(album_id_element, album_id) entry.appendChild(album_id_element) album_name_element: Element = doc.createElement("albumTitle") set_element_value(album_name_element, album_name) entry.appendChild(album_name_element) # Add rights right: Element = doc.createElement("right") right_props = { "providerCode": "117767492:MP3_DOWNLOAD_UENC_256kb_075", "price": 800, "priceCurrencyCode": "MPT", "licenseType": "Preview", "audioEncoding": "MP3", "offerId": "urn:uuid:9534a201-2102-11db-89ca-0019b92a3933", "paymentTypes": { "paymentType": "Microsoft Points" } } set_values_as_elements(doc, right, right_props) rights: Element = doc.createElement("rights") rights.appendChild(right) entry.appendChild(rights) xml_str = doc.toprettyxml(indent="\t") print(xml_str) return Response(xml_str, mimetype=MIME_XML) # Get top tracks @app.route(f"/v3.2//music/chart/zune/tracks/") def music_chart_tracks(locale: str): deezer = api.deezer.get_chart_tracks() if type(deezer) is int: abort(deezer) return doc: Document = minidom.Document() feed: Element = create_feed(doc, "Tracks", "tracks", f"/v3.2/{locale}/music/chart/zune/tracks") for track in deezer: try: recording = musicbrainzngs.search_recordings( f"artistname:{track['artist']['name']} AND release:{track['album']['title']}" f"AND recording:{track['title']}" , 1, True)["recording-list"][0] except ResponseError as res_err: continue except IndexError as idx_err: continue # Set track ID and Title id: str = recording["id"] title: str = recording["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/collection/features/" + id) # Get artist ID and Name artist = recording["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] # Set track information track_props = { "isExplicit": track["explicit_lyrics"], "playRank": track["rank"], "isActionable": True, } set_values_as_elements(doc, entry, track_props) # Set primaryArtist primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) # Add rights right: Element = doc.createElement("right") right_props = { #"providerName": "YouTube", "providerCode": "117767492:MP3_DOWNLOAD_UENC_256kb_075", "price": 800, "priceCurrencyCode": "MPT", "licenseType": "Preview", "audioEncoding": "MP3", "offerId": "urn:uuid:9534a201-2102-11db-89ca-0019b92a3933", "paymentTypes": { "paymentType": "Microsoft Points" } #"offerInstanceId": "urn:uuid:9534a201-2102-11db-89ca-0019b92a3933", } set_values_as_elements(doc, right, right_props) rights: Element = doc.createElement("rights") rights.appendChild(right) entry.appendChild(rights) try: track_position: str = track["position"] index_elem: Element = doc.createElement("trackNumber") set_element_value(index_elem, track_position) entry.appendChild(index_elem) except: pass feed.appendChild(entry) xml_str = doc.toprettyxml(indent="\t") print(xml_str) return Response(xml_str, mimetype=MIME_XML) # Search tracks @app.route(f"/v3.2//music/track") def music_track(locale: str): query: str = request.args.get("q") try: response = musicbrainzngs.search_recordings(query) except ResponseError as error: abort(error.cause.code) return doc: Document = minidom.Document() feed: Element = create_feed(doc, "tracks", "tracks", request.endpoint) for recording in response["recording-list"]: try: # Set track ID and Title id: str = recording["id"] title: str = recording["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/track/" + id) # Get artist ID and Name artist = recording["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] # Set primaryArtist primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) # Get album ID and Title album = recording["release-list"][0] album_id: str = album["id"] album_name: str = album["title"] # Set album info primary_album_elem: Element = doc.createElement("album") primary_album_props = { "id": album_id, "title": album_name } set_values_as_elements(doc, primary_album_elem, primary_album_props) entry.appendChild(primary_album_elem) feed.appendChild(entry) except: pass xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) # Search albums @app.route(f"/v3.2//music/album") def music_album(locale: str): query: str = request.args.get("q") try: response = musicbrainzngs.search_releases(query) except ResponseError as error: abort(error.cause.code) return doc: Document = minidom.Document() feed: Element = create_feed(doc, response["title"], response["id"], f"/v3.2/{locale}/music/chart/zune/albums") for release in response["release-list"]: print(release) # Set track ID and Title id: str = release["id"] title: str = release["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/music/album/" + id) # Add front cover image_elem: Element = doc.createElement("image") image_id_elem: Element = create_id(doc, id) image_elem.appendChild(image_id_elem) entry.appendChild(image_elem) # Get artist ID and Name artist = release["artist-credit"][0]["artist"] artist_id: str = artist["id"] artist_name: str = artist["name"] # Set primaryArtist primary_artist_elem: Element = doc.createElement("primaryArtist") primary_artist_props = { "id": artist_id, "name": artist_name } set_values_as_elements(doc, primary_artist_elem, primary_artist_props) entry.appendChild(primary_artist_elem) feed.appendChild(entry) xml_str = doc.toprettyxml(indent="\t") return Response(xml_str, mimetype=MIME_XML) ### MOVIES @app.route(f"/v3.2//chart/zuneDownload/movie/") def chart_zunedown_movie(locale: str): with open(f'reference/catalog.zune.net_v3.2_{locale}_hubs_music.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) @app.route(f"/v3.2//music/hub/movie/") def hubs_movie(locale: str): imdb = api.imdb.get_chart() if type(imdb) is int: abort(imdb) return doc: Document = minidom.Document() feed: Element = create_feed(doc, "Movies", "movies", f"/v3.2/{locale}/music/hub/movie") for movie in imdb: # Set movie ID and Title id: str = api.imdb.IMDB_GUID_TITLE_BASE + movie["id"][2:] title: str = movie["title"] entry: Element = create_entry(doc, title, id, f"/v3.2/{locale}/movies/" + id) # Set track information track_props = { "imageId": id, "isActionable": True, } set_values_as_elements(doc, entry, track_props) feed.appendChild(entry) xml_str = doc.toprettyxml(indent="\t") print(xml_str) return Response(xml_str, mimetype=MIME_XML) @app.route(f"/v3.2//music/hub/video/") def hubs_video(locale: str): with open(f'reference/catalog.zune.net_v3.2_{locale}_hubs_music.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) ### PODCASTS @app.route(f"/v3.2//music/hub/podcast/") def hubs_podcasts(locale: str): with open(f'reference/catalog.zune.net_v3.2_{locale}_music_hub_podcast.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) @app.route(f"/v3.2//podcast") def podcast_passthrough(locale: str): podcast_url: str = request.args.get('url') podcast = urllib.request.urlopen(podcast_url) return Response(podcast.read(), mimetype="application/rss+xml") ### Metadata # fai.music.metaservices.microsoft.com @app.route("/ZuneAPI/EndPoints.aspx") def get_metadata_endpoints(): print(request.full_path) with open('reference/catalog.zune.net_v3.2_en-US_hubs_music.xml', 'r') as file: data: str = file.read().replace('\n', '') return Response(data, mimetype=MIME_ATOM_XML) if __name__ == "__main__": app.run()