diff --git a/.gitignore b/.gitignore index 5db2268..50cc9d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # generic temporary / dev files *.pyc *.sh -*.txt +*.note *.xcf nohup.out /.dev diff --git a/README.md b/README.md index 39aa026..fba5b56 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ Also neat: You can use your **custom artist or track images**. * [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython) * [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle) * [waitress](https://docs.pylonsproject.org/projects/waitress/) - [GitHub](https://github.com/Pylons/waitress) -* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.7.2) +* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.9.1) +* [nimrodel](https://pypi.org/project/nimrodel/) - [GitHub](https://github.com/krateng/nimrodel) (at least Version 0.4.9) +* [setproctitle](https://pypi.org/project/setproctitle/) - [GitHub](https://github.com/dvarrazzo/py-setproctitle) * If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Fanart.tv](https://fanart.tv/get-an-api-key/). These are free of charge! ## How to install @@ -28,13 +30,15 @@ Also neat: You can use your **custom artist or track images**. ./maloja install -2) Start the server with +2) Install required packages with + + pip3 install -r requirements.txt + +3) Start the server with maloja start - If you're missing packages, the console output will tell you so. Install them. - -2) (Recommended) Put your server behind a reverse proxy for SSL encryption. +4) (Recommended) Put your server behind a reverse proxy for SSL encryption. ## How to use @@ -58,13 +62,15 @@ If you didn't install Maloja from the package (and therefore don't have it in `/ 3) Various folders have `.info` files with more information on how to use their associated features. +4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`. + ## How to scrobble ### Native API If you use Plex Web or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings. -If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key`. +If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as from-data or json. ### Standard-compliant API diff --git a/compliant_api.py b/compliant_api.py index 289af16..24e4019 100644 --- a/compliant_api.py +++ b/compliant_api.py @@ -43,17 +43,12 @@ def handler(apiname,version): return cls return deco -def handle(path,keys,headers,auth): +def handle(path,keys): print("API request: " + str(path)) print("Keys:") for k in keys: - print("\t" + str(k) + ": " + str(keys.get(k))) - print("Headers:") - for h in headers: - print("\t" + str(h) + ": " + str(headers.get(h))) - print("Auth: " + str(auth)) + print("\t",k,":",keys.get(k)) - keys = {**keys,**headers} if len(path)>1 and (path[0],path[1]) in handlers: handler = handlers[(path[0],path[1])] @@ -179,7 +174,7 @@ class LBrnz1(APIHandler): } self.errors = { BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}), - InvalidAuthException:(401,{"code":401,"error":"Bad Auth"}), + InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}), InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}), MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}), ScrobblingException:(500,{"code":500,"error":"Unspecified server error."}) @@ -191,7 +186,7 @@ class LBrnz1(APIHandler): def submit(self,pathnodes,keys): try: - token = keys.get("Authorization").replace("token ","").strip() + token = keys.get("Authorization").replace("token ","").replace("Token ","").strip() except: raise BadAuthException() diff --git a/database.py b/database.py index 0396ec7..65fbc5f 100644 --- a/database.py +++ b/database.py @@ -1,19 +1,23 @@ # server from bottle import request, response, FormsDict # rest of the project -from cleanup import * -from utilities import * -from malojatime import * +from cleanup import CleanerAgent, CollectorAgent +import utilities +from malojatime import register_scrobbletime, time_stamps, ranges from urihandler import uri_to_internal, internal_to_uri, compose_querystring import compliant_api # doreah toolkit from doreah.logging import log from doreah import tsv +from doreah import settings from doreah.caching import Cache, DeepCache try: from doreah.persistence import DiskDict except: pass import doreah +# nimrodel API +from nimrodel import EAPI as API +from nimrodel import Multi # technical import os import datetime @@ -27,7 +31,6 @@ import urllib - dblock = Lock() #global database lock SCROBBLES = [] # Format: tuple(track_ref,timestamp,saved) @@ -182,54 +185,13 @@ def getTrackID(artists,title): ######## -# silly patch to get old syntax working without dbserver - -# function to register all the functions to the real server -def register_subroutes(server,path): - for subpath in dbserver.handlers_get: - func = dbserver.handlers_get[subpath] - decorator = server.get(path + subpath) - decorator(func) - for subpath in dbserver.handlers_post: - func = dbserver.handlers_post[subpath] - decorator = server.post(path + subpath) - decorator(func) +dbserver = API(delay=True,path="api") -# fake server -class FakeBottle: - def __init__(self): - self.handlers_get = {} - self.handlers_post = {} - - # these functions pretend that they're the bottle decorators, but only write - # down which functions asked for them so they can later report their names - # to the real bottle server - def get(self,path): - def register(func): - self.handlers_get[path] = func - return func - return register - def post(self,path): - def register(func): - self.handlers_post[path] = func - return func - return register - - def route(self,path): - return self.get(path) - - -dbserver = FakeBottle() - - - - -@dbserver.route("/test") -def test_server(): - apikey = request.query.get("key") +@dbserver.get("test") +def test_server(key=None): response.set_header("Access-Control-Allow-Origin","*") - if apikey is not None and not (checkAPIkey(apikey)): + if key is not None and not (checkAPIkey(key)): response.status = 403 return "Wrong API key" @@ -247,9 +209,8 @@ def test_server(): ## All database functions are separated - the external wrapper only reads the request keys, converts them into lists and renames them where necessary, and puts the end result in a dict if not already so it can be returned as json -@dbserver.route("/scrobbles") -def get_scrobbles_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("scrobbles") +def get_scrobbles_external(**keys): k_filter, k_time, _, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_amount} @@ -277,9 +238,8 @@ def get_scrobbles(**keys): # return {"scrobbles":len(SCROBBLES),"tracks":len(TRACKS),"artists":len(ARTISTS)} -@dbserver.route("/numscrobbles") -def get_scrobbles_num_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("numscrobbles") +def get_scrobbles_num_external(**keys): k_filter, k_time, _, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_amount} @@ -342,9 +302,8 @@ def get_scrobbles_num(**keys): -@dbserver.route("/tracks") -def get_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("tracks") +def get_tracks_external(**keys): k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -366,7 +325,7 @@ def get_tracks(artist=None): #ls = [t for t in tracklist if (artist in t["artists"]) or (artist==None)] -@dbserver.route("/artists") +@dbserver.get("artists") def get_artists_external(): result = get_artists() return {"list":result} @@ -380,9 +339,8 @@ def get_artists(): -@dbserver.route("/charts/artists") -def get_charts_artists_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("charts/artists") +def get_charts_artists_external(**keys): _, k_time, _, _ = uri_to_internal(keys) ckeys = {**k_time} @@ -397,9 +355,8 @@ def get_charts_artists(**keys): -@dbserver.route("/charts/tracks") -def get_charts_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("charts/tracks") +def get_charts_tracks_external(**keys): k_filter, k_time, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter, **k_time} @@ -417,9 +374,8 @@ def get_charts_tracks(**keys): -@dbserver.route("/pulse") -def get_pulse_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("pulse") +def get_pulse_external(**keys): k_filter, k_time, k_internal, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -440,9 +396,8 @@ def get_pulse(**keys): -@dbserver.route("/performance") -def get_performance_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("performance") +def get_performance_external(**keys): k_filter, k_time, k_internal, k_amount = uri_to_internal(keys) ckeys = {**k_filter, **k_time, **k_internal, **k_amount} @@ -480,10 +435,8 @@ def get_performance(**keys): -@dbserver.route("/top/artists") -def get_top_artists_external(): - - keys = FormsDict.decode(request.query) +@dbserver.get("top/artists") +def get_top_artists_external(**keys): _, k_time, k_internal, _ = uri_to_internal(keys) ckeys = {**k_time, **k_internal} @@ -513,9 +466,8 @@ def get_top_artists(**keys): -@dbserver.route("/top/tracks") -def get_top_tracks_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("top/tracks") +def get_top_tracks_external(**keys): _, k_time, k_internal, _ = uri_to_internal(keys) ckeys = {**k_time, **k_internal} @@ -548,9 +500,8 @@ def get_top_tracks(**keys): -@dbserver.route("/artistinfo") -def artistInfo_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("artistinfo") +def artistInfo_external(**keys): k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True) ckeys = {**k_filter} @@ -560,14 +511,16 @@ def artistInfo_external(): def artistInfo(artist): charts = db_aggregate(by="ARTIST") - scrobbles = len(db_query(artists=[artist])) #we cant take the scrobble number from the charts because that includes all countas scrobbles + scrobbles = len(db_query(artists=[artist])) + #we cant take the scrobble number from the charts because that includes all countas scrobbles try: c = [e for e in charts if e["artist"] == artist][0] others = [a for a in coa.getAllAssociated(artist) if a in ARTISTS] position = c["rank"] return {"scrobbles":scrobbles,"position":position,"associated":others,"medals":MEDALS.get(artist)} except: - # if the artist isnt in the charts, they are not being credited and we need to show information about the credited one + # if the artist isnt in the charts, they are not being credited and we + # need to show information about the credited one artist = coa.getCredited(artist) c = [e for e in charts if e["artist"] == artist][0] position = c["rank"] @@ -577,23 +530,37 @@ def artistInfo(artist): -@dbserver.route("/trackinfo") -def trackInfo_external(): - keys = FormsDict.decode(request.query) +@dbserver.get("trackinfo") +def trackInfo_external(artist:Multi[str],**keys): + # transform into a multidict so we can use our nomral uri_to_internal function + keys = FormsDict(keys) + for a in artist: + keys.append("artist",a) k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True) ckeys = {**k_filter} results = trackInfo(**ckeys) return results -def trackInfo(artists,title): +def trackInfo(track): charts = db_aggregate(by="TRACK") #scrobbles = len(db_query(artists=artists,title=title)) #chart entry of track always has right scrobble number, no countas rules here - c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0] + #c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0] + c = [e for e in charts if e["track"] == track][0] scrobbles = c["scrobbles"] position = c["rank"] + cert = None + threshold_gold, threshold_platinum, threshold_diamond = settings.get_settings("SCROBBLES_GOLD","SCROBBLES_PLATINUM","SCROBBLES_DIAMOND") + if scrobbles >= threshold_diamond: cert = "diamond" + elif scrobbles >= threshold_platinum: cert = "platinum" + elif scrobbles >= threshold_gold: cert = "gold" - return {"scrobbles":scrobbles,"position":position,"medals":MEDALS_TRACKS.get((frozenset(artists),title))} + return { + "scrobbles":scrobbles, + "position":position, + "medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])), + "certification":cert + } @@ -601,9 +568,8 @@ def trackInfo(artists,title): -@dbserver.get("/newscrobble") -def pseudo_post_scrobble(): - keys = FormsDict.decode(request.query) # The Dal★Shabet handler +@dbserver.get("newscrobble") +def pseudo_post_scrobble(**keys): artists = keys.get("artist") title = keys.get("title") apikey = keys.get("key") @@ -626,9 +592,8 @@ def pseudo_post_scrobble(): return {"status":"success","track":trackdict} -@dbserver.post("/newscrobble") -def post_scrobble(): - keys = FormsDict.decode(request.forms) # The Dal★Shabet handler +@dbserver.post("newscrobble") +def post_scrobble(**keys): artists = keys.get("artist") title = keys.get("title") apikey = keys.get("key") @@ -659,28 +624,26 @@ def post_scrobble(): # standard-compliant scrobbling methods -@dbserver.post("/s/") -@dbserver.get("/s/") -def sapi(path): - path = path.split("/") +@dbserver.post("s/{path}",pass_headers=True) +@dbserver.get("s/{path}",pass_headers=True) +def sapi(path:Multi,**keys): + """Scrobbles according to a standardized protocol. + + :param string path: Path according to the scrobble protocol + :param string keys: Query keys according to the scrobble protocol + """ path = list(filter(None,path)) - headers = request.headers - if request.get_header("Content-Type") is not None and "application/json" in request.get_header("Content-Type"): - keys = request.json - else: - keys = FormsDict.decode(request.params) - auth = request.auth - return compliant_api.handle(path,keys,headers,auth) + return compliant_api.handle(path,keys) -@dbserver.route("/sync") +@dbserver.get("sync") def abouttoshutdown(): sync() #sys.exit() -@dbserver.post("/newrule") +@dbserver.post("newrule") def newrule(): keys = FormsDict.decode(request.forms) apikey = keys.pop("key",None) @@ -691,7 +654,7 @@ def newrule(): db_rulestate = False -@dbserver.route("/issues") +@dbserver.get("issues") def issues_external(): #probably not even needed return issues() @@ -787,7 +750,7 @@ def issues(): return {"duplicates":duplicates,"combined":combined,"newartists":newartists,"inconsistent":inconsistent} -@dbserver.post("/importrules") +@dbserver.post("importrules") def import_rulemodule(): keys = FormsDict.decode(request.forms) apikey = keys.pop("key",None) @@ -807,7 +770,7 @@ def import_rulemodule(): -@dbserver.post("/rebuild") +@dbserver.post("rebuild") def rebuild(): keys = FormsDict.decode(request.forms) @@ -827,9 +790,8 @@ def rebuild(): -@dbserver.get("/search") -def search(): - keys = FormsDict.decode(request.query) +@dbserver.get("search") +def search(**keys): query = keys.get("query") max_ = keys.get("max") if max_ is not None: max_ = int(max_) @@ -923,10 +885,10 @@ def build_db(): # coa.updateIDs(ARTISTS) #start regular tasks - update_medals() + utilities.update_medals() global db_rulestate - db_rulestate = consistentRulestate("scrobbles",cla.checksums) + db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums) log("Database fully built!") @@ -959,7 +921,7 @@ def sync(): for e in entries: tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False) #addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False) - combineChecksums("scrobbles/" + e + ".tsv",cla.checksums) + utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums) global lastsync @@ -989,7 +951,7 @@ cacheday = (0,0,0) def db_query(**kwargs): check_cache_age() global cache_query, cache_query_permanent - key = serialize(kwargs) + key = utilities.serialize(kwargs) if "timerange" in kwargs and not kwargs["timerange"].active(): if key in cache_query_permanent: #print("Hit") @@ -1014,7 +976,7 @@ else: def db_aggregate(**kwargs): check_cache_age() global cache_aggregate, cache_aggregate_permanent - key = serialize(kwargs) + key = utilities.serialize(kwargs) if "timerange" in kwargs and not kwargs["timerange"].active(): if key in cache_aggregate_permanent: return copy.copy(cache_aggregate_permanent.get(key)) result = db_aggregate_full(**kwargs) diff --git a/external.py b/external.py index eac30e5..c9c51fc 100644 --- a/external.py +++ b/external.py @@ -5,53 +5,60 @@ from doreah.settings import get_settings from doreah.logging import log -apis_artists = [ - { +apis_artists = [] + +if get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"]: + apis_artists.append({ "name":"LastFM + Fanart.tv", - "check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"], + #"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"], "steps":[ - ("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"), + ("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + str(get_settings("LASTFM_API_KEY")) + "&format=json"), ("parse",["artist","mbid"]), - ("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + get_settings("FANARTTV_API_KEY")), + ("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + str(get_settings("FANARTTV_API_KEY"))), ("parse",["artistthumb",0,"url"]) ] - }, - { + }) + +if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]: + apis_artists.append({ "name":"Spotify", - "check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"], + #"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"], "steps":[ ("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}), ("parse",["access_token"]), ("get","https://api.spotify.com/v1/search?q={artiststring}&type=artist&access_token={var}"), ("parse",["artists","items",0,"images",0,"url"]) ] - } -] + }) -apis_tracks = [ - { +apis_tracks = [] + +if get_settings("LASTFM_API_KEY") not in [None,"ASK"]: + apis_tracks.append({ "name":"LastFM", - "check":get_settings("LASTFM_API_KEY") not in [None,"ASK"], + #"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"], "steps":[ ("get","https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={titlestring}&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"), ("parse",["track","album","image",3,"#text"]) ] - }, - { + }) + +if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]: + apis_tracks.append({ "name":"Spotify", - "check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"], + #"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"], "steps":[ ("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}), ("parse",["access_token"]), ("get","https://api.spotify.com/v1/search?q={artiststring}%20{titlestring}&type=track&access_token={var}"), ("parse",["tracks","items",0,"album","images",0,"url"]) ] - } -] + }) + def api_request_artist(artist): for api in apis_artists: - if api["check"]: + if True: log("API: " + api["name"] + "; Image request: " + artist,module="external") try: artiststring = urllib.parse.quote(artist) @@ -85,7 +92,7 @@ def api_request_artist(artist): def api_request_track(track): artists, title = track for api in apis_tracks: - if api["check"]: + if True: log("API: " + api["name"] + "; Image request: " + "/".join(artists) + " - " + title,module="external") try: artiststring = urllib.parse.quote(", ".join(artists)) diff --git a/htmlgenerators.py b/htmlgenerators.py index 7ca3574..c8b7b5e 100644 --- a/htmlgenerators.py +++ b/htmlgenerators.py @@ -14,8 +14,9 @@ def entity_column(element,counting=[],image=None): if "artists" in element: # track - html += "" + html_links(element["artists"]) + "" - html += "" + html_link(element) + "" + # html += "" + html_links(element["artists"]) + "" + # html += "" + html_link(element) + "" + html += "" + html_links(element["artists"]) + " – " + html_link(element) + "" else: # artist html += "" + html_link(element) diff --git a/maloja b/maloja index e9f5d1f..5fc9eab 100755 --- a/maloja +++ b/maloja @@ -13,7 +13,8 @@ neededmodules = [ "bottle", "waitress", "setproctitle", - "doreah" + "doreah", + "nimrodel" ] recommendedmodules = [ @@ -117,10 +118,10 @@ def install(): if toinstall != [] or toinstallr != []: if os.geteuid() != 0: - print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically. For this, you need to run this script as a root user.") + print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically. For this, you need to run this script as a root user.") return False else: - print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.") + print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.") fail = False if toinstall != []: print("Attempt to install required modules? [Y/n]") @@ -283,7 +284,7 @@ def update(): os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR) - print("Make sure to install the latest version of doreah! (" + yellow("pip3 install --upgrade --no-cache-dir doreah") + ")") + print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")") if stop(): start() #stop returns whether it was running before, in which case we restart it diff --git a/malojatime.py b/malojatime.py index 9b03e70..d789b8c 100644 --- a/malojatime.py +++ b/malojatime.py @@ -66,6 +66,9 @@ class MRangeDescriptor: def unlimited(self): return False + def active(self): + return (self.last_stamp() > datetime.datetime.utcnow().timestamp()) + # returns the description of the range including buttons to go back and forth #def desc_interactive(self,**kwargs): # if self.next(1) is None: @@ -99,7 +102,7 @@ class MTime(MRangeDescriptor): # whether we currently live or will ever again live in this range def active(self): - tod = datetime.date.today() + tod = datetime.datetime.utcnow().date() if tod.year > self.year: return False if self.precision == 1: return True if tod.year == self.year: @@ -235,13 +238,13 @@ class MTimeWeek(MRangeDescriptor): return str(self) # whether we currently live or will ever again live in this range - def active(self): - tod = datetime.date.today() - if tod.year > self.year: return False - if tod.year == self.year: - if tod.chrcalendar()[1] > self.week: return False - - return True +# def active(self): +# tod = datetime.date.today() +# if tod.year > self.year: return False +# if tod.year == self.year: +# if tod.chrcalendar()[1] > self.week: return False +# +# return True def urikeys(self): return {"in":str(self)} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a17d235 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +bottle>=0.12.16 +waitress>=1.3 +doreah>=0.9.1 +nimrodel>=0.4.9 +setproctitle>=1.1.10 +wand>=0.5.4 diff --git a/rules/predefined/krateng_artistsingroups.tsv b/rules/predefined/krateng_artistsingroups.tsv index b60045f..9079410 100644 --- a/rules/predefined/krateng_artistsingroups.tsv +++ b/rules/predefined/krateng_artistsingroups.tsv @@ -5,6 +5,7 @@ countas Selena Gomez & The Scene Selena Gomez countas The Police Sting countas Trouble Maker HyunA +countas S Club 7 Tina Barrett # Group more famous than single artist countas RenoakRhythm Approaching Nirvana countas Shirley Manson Garbage diff --git a/rules/predefined/krateng_kpopgirlgroups.tsv b/rules/predefined/krateng_kpopgirlgroups.tsv index f1d0a95..e07c332 100644 --- a/rules/predefined/krateng_kpopgirlgroups.tsv +++ b/rules/predefined/krateng_kpopgirlgroups.tsv @@ -46,6 +46,9 @@ replacetitle 종이 심장 (Paper Heart) Paper Heart replacetitle 나비 (Butterfly) Butterfly replacetitle Déjà vu Déjà Vu replacetitle 라차타 (LA chA TA) LA chA TA +replacetitle 여우 같은 내 친구 (No More) No More +replacetitle 시그널 (Signal) Signal +replacetitle 미행 (그림자 : Shadow) Shadow # Stellar replaceartist STELLAR Stellar @@ -111,6 +114,7 @@ replacetitle 음오아예 (Um Oh Ah Yeh) (Um Oh Ah Yeh) Um Oh Ah Yeh replacetitle 따끔 (a little bit) A Little Bit # Hello Venus +replaceartist HELLOVENUS Hello Venus replaceartist Hello/Venus Hello Venus # BESTie diff --git a/rules/predefined/krateng_lotr-soundtrack.tsv b/rules/predefined/krateng_lotr-soundtrack.tsv index fd1d9d1..00df3de 100644 --- a/rules/predefined/krateng_lotr-soundtrack.tsv +++ b/rules/predefined/krateng_lotr-soundtrack.tsv @@ -18,5 +18,6 @@ replaceartist Sir James Galway Viggo Mortensen And Renée Fleming Album Version notanartist In Dreams notanartist Aniron Theme For Aragorn And Arwen notanartist Lament for Gandalf -replaceartist James Galway Sir James Galway -replaceartist Ben del Maestro Ben Del Maestro +replaceartist James Galway Sir James Galway +replaceartist Ben del Maestro Ben Del Maestro +replacetitle Anduril Andúril diff --git a/scrobblers/chromium-generic/background.js b/scrobblers/chromium-generic/background.js index 85d7143..9806105 100644 --- a/scrobblers/chromium-generic/background.js +++ b/scrobblers/chromium-generic/background.js @@ -202,7 +202,8 @@ class Controller { // Already played full song while (this.alreadyPlayed > this.currentLength) { this.alreadyPlayed = this.alreadyPlayed - this.currentLength - scrobble(this.currentArtist,this.currentTitle,this.currentLength) + var secondsago = this.alreadyPlayed + scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago) } this.setUpdate() @@ -248,7 +249,8 @@ class Controller { // Already played full song while (this.alreadyPlayed > this.currentLength) { this.alreadyPlayed = this.alreadyPlayed - this.currentLength - scrobble(this.currentArtist,this.currentTitle,this.currentLength) + var secondsago = this.alreadyPlayed + scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago) } this.currentlyPlaying = false @@ -282,17 +284,22 @@ class Controller { -function scrobble(artist,title,seconds) { - console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime") - artiststring = encodeURIComponent(artist) - titlestring = encodeURIComponent(title) +function scrobble(artist,title,seconds,secondsago=0) { + console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime, " + secondsago + " seconds ago") + var artiststring = encodeURIComponent(artist) + var titlestring = encodeURIComponent(title) + var d = new Date() + var time = Math.floor(d.getTime()/1000) - secondsago + //console.log("Time: " + time) + var requestbody = "artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&time=" + time chrome.storage.local.get("apikey",function(result) { APIKEY = result["apikey"] chrome.storage.local.get("serverurl",function(result) { URL = result["serverurl"] var xhttp = new XMLHttpRequest(); xhttp.open("POST",URL + "/api/newscrobble",true); - xhttp.send("artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&key=" + APIKEY) + xhttp.send(requestbody + "&key=" + APIKEY) + //console.log("Sent: " + requestbody) }); }); diff --git a/scrobblers/maloja-scrobbler.zip b/scrobblers/maloja-scrobbler.zip index 2206f9e..709e32e 100644 Binary files a/scrobblers/maloja-scrobbler.zip and b/scrobblers/maloja-scrobbler.zip differ diff --git a/server.py b/server.py index f3fc78b..a219996 100755 --- a/server.py +++ b/server.py @@ -124,6 +124,7 @@ def static_image(pth): @webserver.route("/") @webserver.route("/") @webserver.route("/") +@webserver.route("/") def static(name): response = static_file("website/" + name,root="") response.set_header("Cache-Control", "public, max-age=3600") @@ -214,7 +215,8 @@ setproctitle.setproctitle("Maloja") ## start database database.start_db() -database.register_subroutes(webserver,"/api") +#database.register_subroutes(webserver,"/api") +database.dbserver.mount(server=webserver) log("Starting up Maloja server...") run(webserver, host='::', port=MAIN_PORT, server='waitress') diff --git a/settings/default.ini b/settings/default.ini index 67a11e2..b6661ab 100644 --- a/settings/default.ini +++ b/settings/default.ini @@ -30,6 +30,13 @@ DEFAULT_RANGE_CHARTS_TRACKS = year # can be days, weeks, months, years DEFAULT_RANGE_PULSE = months +[Fluff] + +# how many scrobbles a track needs to aquire this status +SCROBBLES_GOLD = 250 +SCROBBLES_PLATINUM = 500 +SCROBBLES_DIAMOND = 1000 + [Misc] EXPERIMENTAL_FEATURES = no diff --git a/website/css/maloja.css b/website/css/maloja.css index 08dd926..af9ade7 100644 --- a/website/css/maloja.css +++ b/website/css/maloja.css @@ -375,6 +375,12 @@ a.bronze { +img.certrecord { + height:30px; + vertical-align: text-bottom; +} + + /* ** ** @@ -463,12 +469,16 @@ table.list td.icon div { background-position:center; } -table.list td.artists,td.artist,td.title { +table.list td.artists,td.artist,td.title,td.track { min-width:100px; } +table.list td.track span.artist_in_trackcolumn { + color:#bbb; +} + diff --git a/website/media/record_diamond.png b/website/media/record_diamond.png new file mode 100644 index 0000000..0c6b428 Binary files /dev/null and b/website/media/record_diamond.png differ diff --git a/website/media/record_gold.png b/website/media/record_gold.png new file mode 100644 index 0000000..631b31a Binary files /dev/null and b/website/media/record_gold.png differ diff --git a/website/media/record_platinum.png b/website/media/record_platinum.png new file mode 100644 index 0000000..c11f545 Binary files /dev/null and b/website/media/record_platinum.png differ diff --git a/website/robots.txt b/website/robots.txt new file mode 100644 index 0000000..d346249 --- /dev/null +++ b/website/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /api/ diff --git a/website/track.html b/website/track.html index 5dba077..c657a06 100644 --- a/website/track.html +++ b/website/track.html @@ -15,7 +15,7 @@ KEY_ARTISTS
-

KEY_TRACKTITLE

KEY_POSITION +

KEY_TRACKTITLE

KEY_CERTS KEY_POSITION

KEY_SCROBBLES Scrobbles

diff --git a/website/track.py b/website/track.py index 25dafc5..de56107 100644 --- a/website/track.py +++ b/website/track.py @@ -16,11 +16,14 @@ def instructions(keys): imgurl = getTrackImage(track["artists"],track["title"],fast=True) pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else [] - data = database.trackInfo(track["artists"],track["title"]) + data = database.trackInfo(track) scrobblesnum = str(data["scrobbles"]) pos = "#" + str(data["position"]) + html_cert = "" + if data["certification"] is not None: + html_cert = "".format(cert=data["certification"],certc=data["certification"].capitalize()) html_medals = "" if "medals" in data and data["medals"] is not None: @@ -61,6 +64,7 @@ def instructions(keys): "KEY_IMAGEURL":imgurl, "KEY_SCROBBLELINK":compose_querystring(keys), "KEY_MEDALS":html_medals, + "KEY_CERTS":html_cert, "KEY_SCROBBLELIST":html_scrobbles, # pulse "KEY_PULSE_MONTHS":html_pulse_months,