maloja/maloja/thirdparty/__init__.py

269 lines
7.5 KiB
Python

# these different interfaces are for the different roles a third party service
# can fulfill. implementing them adds some generic functionality to attempt to
# actually perform the role, but this will have to be overwritten in most cases.
# functionality is separated into different layers to allow partial override
# also yes, we're using singleton classes for the different providers
# pls don't sue me
import xml.etree.ElementTree as ElementTree
import json
import urllib.parse, urllib.request
import base64
from doreah.logging import log
from threading import BoundedSemaphore
from ..globalconf import malojaconfig
from .. import database
services = {
"proxyscrobble":[],
"import":[],
"metadata":[]
}
# have a limited number of worker threads so we don't completely hog the cpu with
# these requests. they are mostly network bound, so python will happily open up 200 new
# requests and then when all the responses come in we suddenly can't load pages anymore
thirdpartylock = BoundedSemaphore(4)
def import_scrobbles(identifier):
for service in services['import']:
if service.identifier == identifier:
return service.import_scrobbles()
return False
def proxy_scrobble_all(artists,title,timestamp):
for service in services["proxyscrobble"]:
service.scrobble(artists,title,timestamp)
def get_image_track_all(track):
with thirdpartylock:
for service in services["metadata"]:
try:
res = service.get_image_track(track)
if res is not None:
log("Got track image for " + str(track) + " from " + service.name)
return res
else:
log("Could not get track image for " + str(track) + " from " + service.name)
except Exception as e:
log("Error getting track image from " + service.name + ": " + repr(e))
def get_image_artist_all(artist):
with thirdpartylock:
for service in services["metadata"]:
try:
res = service.get_image_artist(artist)
if res is not None:
log("Got artist image for " + str(artist) + " from " + service.name)
return res
else:
log("Could not get artist image for " + str(artist) + " from " + service.name)
except Exception as e:
log("Error getting artist image from " + service.name + ": " + repr(e))
class GenericInterface:
def active_proxyscrobble(self):
return False
def active_import(self):
return False
def active_metadata(self):
return False
settings = {}
proxyscrobble = {}
scrobbleimport = {}
metadata = {}
def __init__(self):
# populate from settings file once on creation
# avoid constant disk access, restart on adding services is acceptable
for key in self.settings:
self.settings[key] = malojaconfig[self.settings[key]]
self.authorize()
# this makes sure that of every class we define, we immediately create an
# instance (de facto singleton). then each instance checks if the requirements
# are met to use that service in each particular role and registers as such
def __init_subclass__(cls,abstract=False):
if not abstract:
s = cls()
if s.active_proxyscrobble():
services["proxyscrobble"].append(s)
#log(cls.name + " registered as proxy scrobble target")
if s.active_import():
services["import"].append(s)
#log(cls.name + " registered as scrobble import source")
if s.active_metadata():
services["metadata"].append(s)
#log(cls.name + " registered as metadata provider")
def authorize(self):
return True
# per default. no authorization is necessary
# wrapper method
def request(self,url,data,responsetype):
response = urllib.request.urlopen(
url,
data=utf(data)
)
responsedata = response.read()
if responsetype == "xml":
data = ElementTree.fromstring(responsedata)
return data
# proxy scrobbler
class ProxyScrobbleInterface(GenericInterface,abstract=True):
proxyscrobble = {
"required_settings":[],
"activated_setting":None
}
# service provides this role only if the setting is active AND all
# necessary auth settings exist
def active_proxyscrobble(self):
return (
all(self.settings[key] not in [None,"ASK",False] for key in self.proxyscrobble["required_settings"]) and
malojaconfig[self.proxyscrobble["activated_setting"]]
)
def scrobble(self,artists,title,timestamp):
response = urllib.request.urlopen(
self.proxyscrobble["scrobbleurl"],
data=utf(self.proxyscrobble_postdata(artists,title,timestamp)))
responsedata = response.read()
if self.proxyscrobble["response_type"] == "xml":
data = ElementTree.fromstring(responsedata)
return self.proxyscrobble_parse_response(data)
# scrobble import
class ImportInterface(GenericInterface,abstract=True):
scrobbleimport = {
"required_settings":[],
"activated_setting":None
}
# service provides this role only if the setting is active AND all
# necessary auth settings exist
def active_import(self):
return (
all(self.settings[key] not in [None,"ASK",False] for key in self.scrobbleimport["required_settings"])
#and malojaconfig[self.scrobbleimport["activated_setting"]]
# registering as import source doesnt do anything on its own, so no need for a setting
)
def import_scrobbles(self):
for scrobble in self.get_remote_scrobbles():
database.incoming_scrobble(
artists=scrobble['artists'],
title=scrobble['title'],
time=scrobble['time']
)
# metadata
class MetadataInterface(GenericInterface,abstract=True):
metadata = {
"required_settings":[],
"activated_setting":None
}
# service provides this role only if the setting is active AND all
# necessary auth settings exist
def active_metadata(self):
return (
all(self.settings[key] not in [None,"ASK",False] for key in self.metadata["required_settings"]) and
self.identifier in malojaconfig["METADATA_PROVIDERS"]
)
def get_image_track(self,track):
artists, title = track
artiststring = urllib.parse.quote(", ".join(artists))
titlestring = urllib.parse.quote(title)
response = urllib.request.urlopen(
self.metadata["trackurl"].format(artist=artiststring,title=titlestring,**self.settings)
)
responsedata = response.read()
if self.metadata["response_type"] == "json":
data = json.loads(responsedata)
imgurl = self.metadata_parse_response_track(data)
else:
imgurl = None
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
return imgurl
def get_image_artist(self,artist):
artiststring = urllib.parse.quote(artist)
response = urllib.request.urlopen(
self.metadata["artisturl"].format(artist=artiststring,**self.settings)
)
responsedata = response.read()
if self.metadata["response_type"] == "json":
data = json.loads(responsedata)
imgurl = self.metadata_parse_response_artist(data)
else:
imgurl = None
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
return imgurl
# default function to parse response by descending down nodes
# override if more complicated
def metadata_parse_response_artist(self,data):
return self._parse_response("response_parse_tree_artist", data)
def metadata_parse_response_track(self,data):
return self._parse_response("response_parse_tree_track", data)
def _parse_response(self, resp, data):
res = data
for node in self.metadata[resp]:
try:
res = res[node]
except Exception:
return None
return res
def postprocess_url(self,url):
url = url.replace("http:","https:",1)
return url
### useful stuff
def utf(st):
return st.encode(encoding="UTF-8")
def b64(inp):
return base64.b64encode(inp)
### actually create everything
__all__ = [
"lastfm",
"spotify",
"musicbrainz",
"audiodb",
"deezer",
"maloja"
]
from . import *
services["metadata"].sort(
key=lambda provider : malojaconfig["METADATA_PROVIDERS"].index(provider.identifier)
)