maloja/maloja/thirdparty/__init__.py

269 lines
7.5 KiB
Python
Raw Normal View History

# 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
2020-07-25 19:38:56 +03:00
import base64
from doreah.logging import log
from threading import BoundedSemaphore
from ..pkg_global.conf import malojaconfig
from .. import database
services = {
"proxyscrobble":[],
"import":[],
"metadata":[]
}
2022-03-06 04:30:29 +03:00
# 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)
2022-03-06 04:30:29 +03:00
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):
2022-03-06 04:30:29 +03:00
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))
2020-07-25 20:34:41 +03:00
def get_image_artist_all(artist):
2022-03-06 04:30:29 +03:00
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:
2021-12-19 23:10:55 +03:00
self.settings[key] = malojaconfig[self.settings[key]]
2021-11-16 20:22:46 +03:00
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)
2022-04-08 18:31:30 +03:00
#log(cls.name + " registered as proxy scrobble target")
if s.active_import():
services["import"].append(s)
2022-04-08 18:31:30 +03:00
#log(cls.name + " registered as scrobble import source")
if s.active_metadata():
2020-07-28 21:33:26 +03:00
services["metadata"].append(s)
2022-04-08 18:31:30 +03:00
#log(cls.name + " registered as metadata provider")
2020-07-25 19:38:56 +03:00
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 (
2021-11-27 22:04:13 +03:00
all(self.settings[key] not in [None,"ASK",False] for key in self.proxyscrobble["required_settings"]) and
2021-12-19 23:10:55 +03:00
malojaconfig[self.proxyscrobble["activated_setting"]]
)
def scrobble(self,artists,title,timestamp):
response = urllib.request.urlopen(
self.proxyscrobble["scrobbleurl"],
2020-07-25 06:29:23 +03:00
data=utf(self.proxyscrobble_postdata(artists,title,timestamp)))
responsedata = response.read()
if self.proxyscrobble["response_type"] == "xml":
data = ElementTree.fromstring(responsedata)
2020-07-25 06:29:23 +03:00
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
)
2022-01-01 06:25:03 +03:00
def import_scrobbles(self):
for scrobble in self.get_remote_scrobbles():
database.incoming_scrobble(
2022-01-01 06:25:03 +03:00
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 (
2021-11-27 22:04:13 +03:00
all(self.settings[key] not in [None,"ASK",False] for key in self.metadata["required_settings"]) and
2021-12-19 23:10:55 +03:00
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)
2020-08-05 20:31:12 +03:00
imgurl = self.metadata_parse_response_track(data)
else:
imgurl = None
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
return imgurl
2020-07-25 20:34:41 +03:00
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)
2020-08-05 20:31:12 +03:00
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
2020-07-25 20:34:41 +03:00
def metadata_parse_response_artist(self,data):
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
return self._parse_response("response_parse_tree_artist", data)
2020-07-25 20:34:41 +03:00
def metadata_parse_response_track(self,data):
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
return self._parse_response("response_parse_tree_track", data)
def _parse_response(self, resp, data):
res = data
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
for node in self.metadata[resp]:
try:
res = res[node]
except:
return None
return res
2020-08-05 20:31:12 +03:00
def postprocess_url(self,url):
url = url.replace("http:","https:",1)
return url
### useful stuff
def utf(st):
return st.encode(encoding="UTF-8")
2020-07-25 19:38:56 +03:00
def b64(inp):
return base64.b64encode(inp)
### actually create everything
__all__ = [
2020-07-25 19:38:56 +03:00
"lastfm",
"spotify",
"musicbrainz",
"audiodb",
"deezer",
"maloja"
]
2020-07-25 06:29:23 +03:00
from . import *
2020-07-28 21:33:26 +03:00
services["metadata"].sort(
2021-12-19 23:10:55 +03:00
key=lambda provider : malojaconfig["METADATA_PROVIDERS"].index(provider.identifier)
2020-07-28 21:33:26 +03:00
)