Files
PyZuneCatalogServer/app.py
T

658 lines
23 KiB
Python

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/<string:locale>/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/<string:locale>/music/genre/<string:id>/")
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/<string:locale>/music/genre/<string:z_genre_id>/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/<string:locale>/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/<string:locale>/music/album/<string:album_id>/")
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/<string:locale>/music/album/<string:id>/<path:fragment>/")
def music_album_details(id: str, fragment: str, locale: str):
return fragment
# Get artist information
@app.route(f"/v3.2/<string:locale>/music/artist/<string:id>/")
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/<string:locale>/music/artist/<string:artist_id>/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/<string:locale>/music/artist/<string:artist_id>/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/<string:locale>/music/artist/<string:id>/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/<string:locale>/music/track/<string:mbid>/")
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/<string:locale>/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/<string:locale>/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/<string:locale>/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/<string:locale>/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/<string:locale>/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/<string:locale>/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/<string:locale>/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/<string:locale>/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()