Merge branch 'master' into pyhp

This commit is contained in:
Krateng 2019-10-24 15:52:06 +02:00
commit b955777637
57 changed files with 1339 additions and 399 deletions

14
.doreah
View File

@ -1,4 +1,10 @@
logging.logfolder = logs logging:
settings.files = [ "settings/default.ini" , "settings/settings.ini" ] logfolder: "logs"
caching.folder = "cache/" settings:
regular.autostart = false files:
- "settings/default.ini"
- "settings/settings.ini"
caching:
folder: "cache/"
regular:
autostart: false

3
.gitignore vendored
View File

@ -1,6 +1,7 @@
# generic temporary / dev files # generic temporary / dev files
*.pyc *.pyc
*.sh *.sh
!/update_requirements.sh
*.note *.note
*.xcf *.xcf
nohup.out nohup.out
@ -10,10 +11,10 @@ nohup.out
*.tsv *.tsv
*.rulestate *.rulestate
*.log *.log
*.css
# currently not using # currently not using
/screenshot*.png /screenshot*.png
/proxyscrobble.py
# only for development, normally external # only for development, normally external
/doreah /doreah

View File

@ -16,12 +16,8 @@ Also neat: You can use your **custom artist or track images**.
## Requirements ## Requirements
* [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython) * Python 3
* [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle) * Pip packages specified in `requirements.txt`
* [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.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! * 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 ## How to install
@ -68,9 +64,9 @@ If you didn't install Maloja from the package (and therefore don't have it in `/
### Native API ### 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 use Plex Web, Spotify, Bandcamp, Soundcloud 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` - either as from-data or json. 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 form-data or json.
### Standard-compliant API ### Standard-compliant API

View File

@ -1,6 +1,6 @@
import re import re
import utilities import utilities
from doreah import tsv from doreah import tsv, settings
# need to do this as a class so it can retain loaded settings from file # need to do this as a class so it can retain loaded settings from file
# apparently this is not true # apparently this is not true
@ -11,11 +11,16 @@ class CleanerAgent:
self.updateRules() self.updateRules()
def updateRules(self): def updateRules(self):
raw = tsv.parse_all("rules","string","string","string") raw = tsv.parse_all("rules","string","string","string","string")
self.rules_belongtogether = [b for [a,b,c] in raw if a=="belongtogether"] self.rules_belongtogether = [b for [a,b,c,d] in raw if a=="belongtogether"]
self.rules_notanartist = [b for [a,b,c] in raw if a=="notanartist"] self.rules_notanartist = [b for [a,b,c,d] in raw if a=="notanartist"]
self.rules_replacetitle = {b.lower():c for [a,b,c] in raw if a=="replacetitle"} self.rules_replacetitle = {b.lower():c for [a,b,c,d] in raw if a=="replacetitle"}
self.rules_replaceartist = {b.lower():c for [a,b,c] in raw if a=="replaceartist"} self.rules_replaceartist = {b.lower():c for [a,b,c,d] in raw if a=="replaceartist"}
self.rules_ignoreartist = [b.lower() for [a,b,c,d] in raw if a=="ignoreartist"]
self.rules_addartists = {c.lower():(b.lower(),d) for [a,b,c,d] in raw if a=="addartists"}
#self.rules_regexartist = [[b,c] for [a,b,c,d] in raw if a=="regexartist"]
#self.rules_regextitle = [[b,c] for [a,b,c,d] in raw if a=="regextitle"]
# TODO
# we always need to be able to tell if our current database is made with the current rules # we always need to be able to tell if our current database is made with the current rules
self.checksums = utilities.checksumTSV("rules") self.checksums = utilities.checksumTSV("rules")
@ -27,6 +32,12 @@ class CleanerAgent:
title = self.parseTitle(self.removespecial(title)) title = self.parseTitle(self.removespecial(title))
(title,moreartists) = self.parseTitleForArtists(title) (title,moreartists) = self.parseTitleForArtists(title)
artists += moreartists artists += moreartists
if title.lower() in self.rules_addartists:
reqartists, allartists = self.rules_addartists[title.lower()]
reqartists = reqartists.split("")
allartists = allartists.split("")
if set(reqartists).issubset(set(a.lower() for a in artists)):
artists += allartists
artists = list(set(artists)) artists = list(set(artists))
artists.sort() artists.sort()
@ -52,6 +63,12 @@ class CleanerAgent:
def parseArtists(self,a): def parseArtists(self,a):
if a.strip() in settings.get_settings("INVALID_ARTISTS"):
return []
if a.strip().lower() in self.rules_ignoreartist:
return []
if a.strip() == "": if a.strip() == "":
return [] return []

View File

@ -68,6 +68,7 @@ def handle(path,keys):
def scrobbletrack(artiststr,titlestr,timestamp): def scrobbletrack(artiststr,titlestr,timestamp):
try: try:
log("Incoming scrobble (compliant API): ARTISTS: " + artiststr + ", TRACK: " + titlestr,module="debug")
(artists,title) = cla.fullclean(artiststr,titlestr) (artists,title) = cla.fullclean(artiststr,titlestr)
database.createScrobble(artists,title,timestamp) database.createScrobble(artists,title,timestamp)
database.sync() database.sync()

View File

@ -6,6 +6,7 @@ import utilities
from malojatime import register_scrobbletime, time_stamps, ranges from malojatime import register_scrobbletime, time_stamps, ranges
from urihandler import uri_to_internal, internal_to_uri, compose_querystring from urihandler import uri_to_internal, internal_to_uri, compose_querystring
import compliant_api import compliant_api
from external import proxy_scrobble
# doreah toolkit # doreah toolkit
from doreah.logging import log from doreah.logging import log
from doreah import tsv from doreah import tsv
@ -49,8 +50,11 @@ TRACKS_LOWER = []
ARTISTS_LOWER = [] ARTISTS_LOWER = []
ARTIST_SET = set() ARTIST_SET = set()
TRACK_SET = set() TRACK_SET = set()
MEDALS = {} #literally only changes once per year, no need to calculate that on the fly MEDALS = {} #literally only changes once per year, no need to calculate that on the fly
MEDALS_TRACKS = {} MEDALS_TRACKS = {}
WEEKLY_TOPTRACKS = {}
WEEKLY_TOPARTISTS = {}
cla = CleanerAgent() cla = CleanerAgent()
coa = CollectorAgent() coa = CollectorAgent()
@ -73,7 +77,12 @@ def loadAPIkeys():
log("Authenticated Machines: " + ", ".join([m[1] for m in clients])) log("Authenticated Machines: " + ", ".join([m[1] for m in clients]))
def checkAPIkey(k): def checkAPIkey(k):
return (k in [k for [k,d] in clients]) #return (k in [k for [k,d] in clients])
for key, identifier in clients:
if key == k: return identifier
return False
def allAPIkeys(): def allAPIkeys():
return [k for [k,d] in clients] return [k for [k,d] in clients]
@ -102,10 +111,23 @@ def get_track_dict(o):
def createScrobble(artists,title,time,volatile=False): def createScrobble(artists,title,time,volatile=False):
if len(artists) == 0 or title == "":
return {}
dblock.acquire() dblock.acquire()
i = getTrackID(artists,title)
# idempotence
if time in SCROBBLESDICT:
if i == SCROBBLESDICT[time].track:
dblock.release()
return get_track_dict(TRACKS[i])
# timestamp as unique identifier
while (time in SCROBBLESDICT): while (time in SCROBBLESDICT):
time += 1 time += 1
i = getTrackID(artists,title)
obj = Scrobble(i,time,volatile) # if volatile generated, we simply pretend we have already saved it to disk obj = Scrobble(i,time,volatile) # if volatile generated, we simply pretend we have already saved it to disk
#SCROBBLES.append(obj) #SCROBBLES.append(obj)
# immediately insert scrobble correctly so we can guarantee sorted list # immediately insert scrobble correctly so we can guarantee sorted list
@ -116,6 +138,8 @@ def createScrobble(artists,title,time,volatile=False):
invalidate_caches() invalidate_caches()
dblock.release() dblock.release()
proxy_scrobble(artists,title,time)
return get_track_dict(TRACKS[obj.track]) return get_track_dict(TRACKS[obj.track])
@ -225,7 +249,22 @@ def get_scrobbles(**keys):
# return r # return r
return r return r
# info for comparison
@dbserver.get("info")
def info_external(**keys):
result = info()
return result
def info():
totalscrobbles = get_scrobbles_num()
artists = {}
return {
"name":settings.get_settings("NAME"),
"artists":{
chartentry["artist"]:round(chartentry["scrobbles"] * 100 / totalscrobbles,3)
for chartentry in get_charts_artists() if chartentry["scrobbles"]/totalscrobbles >= 0}
}
@ -517,7 +556,14 @@ def artistInfo(artist):
c = [e for e in charts if e["artist"] == artist][0] c = [e for e in charts if e["artist"] == artist][0]
others = [a for a in coa.getAllAssociated(artist) if a in ARTISTS] others = [a for a in coa.getAllAssociated(artist) if a in ARTISTS]
position = c["rank"] position = c["rank"]
return {"scrobbles":scrobbles,"position":position,"associated":others,"medals":MEDALS.get(artist)} performance = get_performance(artist=artist,step="week")
return {
"scrobbles":scrobbles,
"position":position,
"associated":others,
"medals":MEDALS.get(artist),
"topweeks":WEEKLY_TOPARTISTS.get(artist,0)
}
except: except:
# if the artist isnt in the charts, they are not being credited and we # if the artist isnt in the charts, they are not being credited and we
# need to show information about the credited one # need to show information about the credited one
@ -555,11 +601,13 @@ def trackInfo(track):
elif scrobbles >= threshold_platinum: cert = "platinum" elif scrobbles >= threshold_platinum: cert = "platinum"
elif scrobbles >= threshold_gold: cert = "gold" elif scrobbles >= threshold_gold: cert = "gold"
return { return {
"scrobbles":scrobbles, "scrobbles":scrobbles,
"position":position, "position":position,
"medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])), "medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])),
"certification":cert "certification":cert,
"topweeks":WEEKLY_TOPTRACKS.get(((frozenset(track["artists"]),track["title"])),0)
} }
@ -573,13 +621,16 @@ def pseudo_post_scrobble(**keys):
artists = keys.get("artist") artists = keys.get("artist")
title = keys.get("title") title = keys.get("title")
apikey = keys.get("key") apikey = keys.get("key")
if not (checkAPIkey(apikey)): client = checkAPIkey(apikey)
if client == False: # empty string allowed!
response.status = 403 response.status = 403
return "" return ""
try: try:
time = int(keys.get("time")) time = int(keys.get("time"))
except: except:
time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Incoming scrobble (native API): Client " + client + ", ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug")
(artists,title) = cla.fullclean(artists,title) (artists,title) = cla.fullclean(artists,title)
## this is necessary for localhost testing ## this is necessary for localhost testing
@ -587,8 +638,9 @@ def pseudo_post_scrobble(**keys):
trackdict = createScrobble(artists,title,time) trackdict = createScrobble(artists,title,time)
if (time - lastsync) > 3600: sync()
sync()
return {"status":"success","track":trackdict} return {"status":"success","track":trackdict}
@ -597,7 +649,8 @@ def post_scrobble(**keys):
artists = keys.get("artist") artists = keys.get("artist")
title = keys.get("title") title = keys.get("title")
apikey = keys.get("key") apikey = keys.get("key")
if not (checkAPIkey(apikey)): client = checkAPIkey(apikey)
if client == False: # empty string allowed!
response.status = 403 response.status = 403
return "" return ""
@ -605,6 +658,8 @@ def post_scrobble(**keys):
time = int(keys.get("time")) time = int(keys.get("time"))
except: except:
time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Incoming scrobble (native API): Client " + client + ", ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug")
(artists,title) = cla.fullclean(artists,title) (artists,title) = cla.fullclean(artists,title)
## this is necessary for localhost testing ## this is necessary for localhost testing
@ -612,12 +667,11 @@ def post_scrobble(**keys):
trackdict = createScrobble(artists,title,time) trackdict = createScrobble(artists,title,time)
#if (time - lastsync) > 3600:
# sync()
sync() sync()
#always sync, one filesystem access every three minutes shouldn't matter #always sync, one filesystem access every three minutes shouldn't matter
return {"status":"success","track":trackdict} return {"status":"success","track":trackdict}
@ -644,8 +698,7 @@ def abouttoshutdown():
#sys.exit() #sys.exit()
@dbserver.post("newrule") @dbserver.post("newrule")
def newrule(): def newrule(**keys):
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None) apikey = keys.pop("key",None)
if (checkAPIkey(apikey)): if (checkAPIkey(apikey)):
tsv.add_entry("rules/webmade.tsv",[k for k in keys]) tsv.add_entry("rules/webmade.tsv",[k for k in keys])
@ -751,8 +804,7 @@ def issues():
@dbserver.post("importrules") @dbserver.post("importrules")
def import_rulemodule(): def import_rulemodule(**keys):
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None) apikey = keys.pop("key",None)
if (checkAPIkey(apikey)): if (checkAPIkey(apikey)):
@ -771,9 +823,7 @@ def import_rulemodule():
@dbserver.post("rebuild") @dbserver.post("rebuild")
def rebuild(): def rebuild(**keys):
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None) apikey = keys.pop("key",None)
if (checkAPIkey(apikey)): if (checkAPIkey(apikey)):
log("Database rebuild initiated!") log("Database rebuild initiated!")
@ -886,6 +936,7 @@ def build_db():
#start regular tasks #start regular tasks
utilities.update_medals() utilities.update_medals()
utilities.update_weekly()
global db_rulestate global db_rulestate
db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums) db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums)
@ -899,6 +950,7 @@ def sync():
# all entries by file collected # all entries by file collected
# so we don't open the same file for every entry # so we don't open the same file for every entry
#log("Syncing",module="debug")
entries = {} entries = {}
for idx in range(len(SCROBBLES)): for idx in range(len(SCROBBLES)):
@ -918,15 +970,19 @@ def sync():
SCROBBLES[idx] = (SCROBBLES[idx][0],SCROBBLES[idx][1],True) SCROBBLES[idx] = (SCROBBLES[idx][0],SCROBBLES[idx][1],True)
#log("Sorted into months",module="debug")
for e in entries: for e in entries:
tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False) tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False)
#addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False) #addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False)
utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums) utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
#log("Written files",module="debug")
global lastsync global lastsync
lastsync = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) lastsync = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Database saved to disk.") #log("Database saved to disk.")
# save cached images # save cached images
#saveCache() #saveCache()

View File

@ -3,6 +3,10 @@ import json
import base64 import base64
from doreah.settings import get_settings from doreah.settings import get_settings
from doreah.logging import log from doreah.logging import log
import hashlib
import xml.etree.ElementTree as ET
### PICTURES
apis_artists = [] apis_artists = []
@ -130,3 +134,49 @@ def api_request_track(track):
pass pass
return None return None
### SCROBBLING
# creates signature and returns full query string
def lfmbuild(parameters):
m = hashlib.md5()
keys = sorted(str(k) for k in parameters)
m.update(utf("".join(str(k) + str(parameters[k]) for k in keys)))
m.update(utf(get_settings("LASTFM_API_SECRET")))
sig = m.hexdigest()
return urllib.parse.urlencode(parameters) + "&api_sig=" + sig
def utf(st):
return st.encode(encoding="UTF-8")
apis_scrobble = []
if get_settings("LASTFM_API_SK") not in [None,"ASK"] and get_settings("LASTFM_API_SECRET") not in [None,"ASK"] and get_settings("LASTFM_API_KEY") not in [None,"ASK"]:
apis_scrobble.append({
"name":"LastFM",
"scrobbleurl":"http://ws.audioscrobbler.com/2.0/",
"requestbody":lambda artists,title,timestamp: lfmbuild({"method":"track.scrobble","artist[0]":", ".join(artists),"track[0]":title,"timestamp":timestamp,"api_key":get_settings("LASTFM_API_KEY"),"sk":get_settings("LASTFM_API_SK")})
})
def proxy_scrobble(artists,title,timestamp):
for api in apis_scrobble:
response = urllib.request.urlopen(api["scrobbleurl"],data=utf(api["requestbody"](artists,title,timestamp)))
xml = response.read()
data = ET.fromstring(xml)
if data.attrib.get("status") == "ok":
if data.find("scrobbles").attrib.get("ignored") == "0":
log(api["name"] + ": Scrobble accepted: " + "/".join(artists) + " - " + title)
else:
log(api["name"] + ": Scrobble not accepted: " + "/".join(artists) + " - " + title)

View File

@ -2,6 +2,8 @@ import urllib
from bottle import FormsDict from bottle import FormsDict
import datetime import datetime
from urihandler import compose_querystring from urihandler import compose_querystring
import urllib.parse
from doreah.settings import get_settings
# returns the proper column(s) for an artist or track # returns the proper column(s) for an artist or track
@ -16,7 +18,9 @@ def entity_column(element,counting=[],image=None):
# track # track
# html += "<td class='artists'>" + html_links(element["artists"]) + "</td>" # html += "<td class='artists'>" + html_links(element["artists"]) + "</td>"
# html += "<td class='title'>" + html_link(element) + "</td>" # html += "<td class='title'>" + html_link(element) + "</td>"
html += "<td class='track'><span class='artist_in_trackcolumn'>" + html_links(element["artists"]) + "</span> " + html_link(element) + "</td>" html += "<td class='track'><span class='artist_in_trackcolumn'>"
html += trackSearchLink(element)
html += html_links(element["artists"]) + "</span> " + html_link(element) + "</td>"
else: else:
# artist # artist
html += "<td class='artist'>" + html_link(element) html += "<td class='artist'>" + html_link(element)
@ -74,6 +78,33 @@ def trackLink(track):
#artists,title = track["artists"],track["title"] #artists,title = track["artists"],track["title"]
#return "<a href='/track?title=" + urllib.parse.quote(title) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in artists]) + "'>" + title + "</a>" #return "<a href='/track?title=" + urllib.parse.quote(title) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in artists]) + "'>" + title + "</a>"
def trackSearchLink(track):
searchProvider = get_settings("TRACK_SEARCH_PROVIDER")
if searchProvider is None: return ""
link = "<a class='trackProviderSearch' href='"
if searchProvider == "YouTube":
link += "https://www.youtube.com/results?search_query="
elif searchProvider == "YouTube Music":
link += "https://music.youtube.com/search?q="
elif searchProvider == "Google Play Music":
link += "https://play.google.com/music/listen#/srs/"
elif searchProvider == "Spotify":
link += "https://open.spotify.com/search/results/"
elif searchProvider == "Tidal":
link += "https://listen.tidal.com/search/tracks?q="
elif searchProvider == "SoundCloud":
link += "https://soundcloud.com/search?q="
elif searchProvider == "Amazon Music":
link += "https://music.amazon.com/search/"
elif searchProvider == "Deezer":
link += "https://www.deezer.com/search/"
else:
link += "https://www.google.com/search?q=" # ¯\_(ツ)_/¯
link += urllib.parse.quote(", ".join(track["artists"]) + " " + track["title"]) + "'>&#127925;</a>"
return link
#def scrobblesTrackLink(artists,title,timekeys,amount=None,pixels=None): #def scrobblesTrackLink(artists,title,timekeys,amount=None,pixels=None):
def scrobblesTrackLink(track,timekeys,amount=None,percent=None): def scrobblesTrackLink(track,timekeys,amount=None,percent=None):
artists,title = track["artists"],track["title"] artists,title = track["artists"],track["title"]

View File

@ -18,29 +18,38 @@ import math
# result.append(element.get("image")) # result.append(element.get("image"))
# artist=None,track=None,since=None,to=None,within=None,associated=False,max_=None,pictures=False #max_ indicates that no pagination should occur (because this is not the primary module)
def module_scrobblelist(max_=None,pictures=False,shortTimeDesc=False,earlystop=False,**kwargs): def module_scrobblelist(page=0,perpage=100,max_=None,pictures=False,shortTimeDesc=False,earlystop=False,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","track","associated") kwargs_filter = pickKeys(kwargs,"artist","track","associated")
kwargs_time = pickKeys(kwargs,"timerange","since","to","within") kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
# if earlystop, we don't care about the actual amount and only request as many from the db # if earlystop, we don't care about the actual amount and only request as many from the db
# without, we request everything and filter on site # without, we request everything and filter on site
maxkey = {"max_":max_} if earlystop else {} maxkey = {"max_":lastindex} if earlystop else {}
scrobbles = database.get_scrobbles(**kwargs_time,**kwargs_filter,**maxkey) scrobbles = database.get_scrobbles(**kwargs_time,**kwargs_filter,**maxkey)
if pictures: if pictures:
scrobbleswithpictures = scrobbles if max_ is None else scrobbles[:max_] scrobbleswithpictures = [""] * firstindex + scrobbles[firstindex:lastindex]
#scrobbleimages = [e.get("image") for e in getTracksInfo(scrobbleswithpictures)] #will still work with scrobble objects as they are a technically a subset of track objects #scrobbleimages = [e.get("image") for e in getTracksInfo(scrobbleswithpictures)] #will still work with scrobble objects as they are a technically a subset of track objects
#scrobbleimages = ["/image?title=" + urllib.parse.quote(t["title"]) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in t["artists"]]) for t in scrobbleswithpictures] #scrobbleimages = ["/image?title=" + urllib.parse.quote(t["title"]) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in t["artists"]]) for t in scrobbleswithpictures]
scrobbleimages = [getTrackImage(t["artists"],t["title"],fast=True) for t in scrobbleswithpictures] scrobbleimages = [getTrackImage(t["artists"],t["title"],fast=True) for t in scrobbleswithpictures]
pages = math.ceil(len(scrobbles) / perpage)
representative = scrobbles[0] if len(scrobbles) is not 0 else None representative = scrobbles[0] if len(scrobbles) is not 0 else None
# build list # build list
i = 0 i = 0
html = "<table class='list'>" html = "<table class='list'>"
for s in scrobbles: for s in scrobbles:
if i<firstindex:
i += 1
continue
html += "<tr>" html += "<tr>"
html += "<td class='time'>" + timestamp_desc(s["time"],short=shortTimeDesc) + "</td>" html += "<td class='time'>" + timestamp_desc(s["time"],short=shortTimeDesc) + "</td>"
@ -48,32 +57,38 @@ def module_scrobblelist(max_=None,pictures=False,shortTimeDesc=False,earlystop=F
img = scrobbleimages[i] img = scrobbleimages[i]
else: img = None else: img = None
html += entity_column(s,image=img) html += entity_column(s,image=img)
# Alternative way: Do it in one cell
#html += "<td class='title'><span>" + artistLinks(s["artists"]) + "</span> — " + trackLink({"artists":s["artists"],"title":s["title"]}) + "</td>"
html += "</tr>" html += "</tr>"
i += 1 i += 1
if max_ is not None and i>=max_: if i>=lastindex:
break break
html += "</table>" html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html,len(scrobbles),representative) return (html,len(scrobbles),representative)
def module_pulse(max_=None,**kwargs): def module_pulse(page=0,perpage=100,max_=None,**kwargs):
from doreah.timing import clock, clockp from doreah.timing import clock, clockp
kwargs_filter = pickKeys(kwargs,"artist","track","associated") kwargs_filter = pickKeys(kwargs,"artist","track","associated")
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail") kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
ranges = database.get_pulse(**kwargs_time,**kwargs_filter) ranges = database.get_pulse(**kwargs_time,**kwargs_filter)
pages = math.ceil(len(ranges) / perpage)
if max_ is not None: ranges = ranges[:max_] ranges = ranges[firstindex:lastindex]
# if time range not explicitly specified, only show from first appearance # if time range not explicitly specified, only show from first appearance
# if "since" not in kwargs: # if "since" not in kwargs:
@ -94,19 +109,27 @@ def module_pulse(max_=None,**kwargs):
html += "</tr>" html += "</tr>"
html += "</table>" html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return html return html
def module_performance(max_=None,**kwargs): def module_performance(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","track") kwargs_filter = pickKeys(kwargs,"artist","track")
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail") kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
ranges = database.get_performance(**kwargs_time,**kwargs_filter) ranges = database.get_performance(**kwargs_time,**kwargs_filter)
if max_ is not None: ranges = ranges[:max_] pages = math.ceil(len(ranges) / perpage)
ranges = ranges[firstindex:lastindex]
# if time range not explicitly specified, only show from first appearance # if time range not explicitly specified, only show from first appearance
# if "since" not in kwargs: # if "since" not in kwargs:
@ -130,18 +153,26 @@ def module_performance(max_=None,**kwargs):
html += "</tr>" html += "</tr>"
html += "</table>" html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return html return html
def module_trackcharts(max_=None,**kwargs): def module_trackcharts(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","associated") kwargs_filter = pickKeys(kwargs,"artist","associated")
kwargs_time = pickKeys(kwargs,"timerange","since","to","within") kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
tracks = database.get_charts_tracks(**kwargs_filter,**kwargs_time) tracks = database.get_charts_tracks(**kwargs_filter,**kwargs_time)
pages = math.ceil(len(tracks) / perpage)
# last time range (to compare) # last time range (to compare)
try: try:
trackslast = database.get_charts_tracks(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1)) trackslast = database.get_charts_tracks(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1))
@ -167,13 +198,16 @@ def module_trackcharts(max_=None,**kwargs):
i = 0 i = 0
html = "<table class='list'>" html = "<table class='list'>"
for e in tracks: for e in tracks:
if i<firstindex:
i += 1
continue
i += 1 i += 1
if max_ is not None and i>max_: if i>lastindex:
break break
html += "<tr>" html += "<tr>"
# rank # rank
if i == 1 or e["scrobbles"] < prev["scrobbles"]: if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(i) + "</td>" html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
else: else:
html += "<td class='rank'></td>" html += "<td class='rank'></td>"
# rank change # rank change
@ -196,16 +230,26 @@ def module_trackcharts(max_=None,**kwargs):
prev = e prev = e
html += "</table>" html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html,representative) return (html,representative)
def module_artistcharts(max_=None,**kwargs): def module_artistcharts(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"associated") #not used right now kwargs_filter = pickKeys(kwargs,"associated") #not used right now
kwargs_time = pickKeys(kwargs,"timerange","since","to","within") kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
artists = database.get_charts_artists(**kwargs_filter,**kwargs_time) artists = database.get_charts_artists(**kwargs_filter,**kwargs_time)
pages = math.ceil(len(artists) / perpage)
# last time range (to compare) # last time range (to compare)
try: try:
#from malojatime import _get_next #from malojatime import _get_next
@ -231,13 +275,16 @@ def module_artistcharts(max_=None,**kwargs):
i = 0 i = 0
html = "<table class='list'>" html = "<table class='list'>"
for e in artists: for e in artists:
if i<firstindex:
i += 1
continue
i += 1 i += 1
if max_ is not None and i>max_: if i>lastindex:
break break
html += "<tr>" html += "<tr>"
# rank # rank
if i == 1 or e["scrobbles"] < prev["scrobbles"]: if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(i) + "</td>" html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
else: else:
html += "<td class='rank'></td>" html += "<td class='rank'></td>"
# rank change # rank change
@ -262,6 +309,8 @@ def module_artistcharts(max_=None,**kwargs):
html += "</table>" html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html, representative) return (html, representative)
@ -308,7 +357,7 @@ def module_toptracks(pictures=True,**kwargs):
if pictures: if pictures:
html += "<td><div></div></td>" html += "<td><div></div></td>"
html += "<td class='stats'>" + "No scrobbles" + "</td>" html += "<td class='stats'>" + "No scrobbles" + "</td>"
html += "<td>" + "" + "</td>" #html += "<td>" + "" + "</td>"
html += "<td class='amount'>" + "0" + "</td>" html += "<td class='amount'>" + "0" + "</td>"
html += "<td class='bar'>" + "" + "</td>" html += "<td class='bar'>" + "" + "</td>"
else: else:
@ -478,22 +527,60 @@ def module_trackcharts_tiles(**kwargs):
return html return html
def module_paginate(page,pages,perpage,**keys):
unchangedkeys = internal_to_uri({**keys,"perpage":perpage})
html = "<div class='paginate'>"
if page > 1:
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":0})) + "'><span class='stat_selector'>" + "1" + "</span></a>"
html += " | "
if page > 2:
html += " ... | "
if page > 0:
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page-1})) + "'><span class='stat_selector'>" + str(page) + "</span></a>"
html += " « "
html += "<span style='opacity:0.5;' class='stat_selector'>" + str(page+1) + "</span>"
if page < pages-1:
html += " » "
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page+1})) + "'><span class='stat_selector'>" + str(page+2) + "</span></a>"
if page < pages-3:
html += " | ... "
if page < pages-2:
html += " | "
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":pages-1})) + "'><span class='stat_selector'>" + str(pages) + "</span></a>"
html += "</div>"
return html
# THIS FUNCTION USES THE ORIGINAL URI KEYS!!! # THIS FUNCTION USES THE ORIGINAL URI KEYS!!!
def module_filterselection(keys,time=True,delimit=False): def module_filterselection(keys,time=True,delimit=False):
filterkeys, timekeys, delimitkeys, extrakeys = uri_to_internal(keys) from malojatime import today, thisweek, thismonth, thisyear, alltime
filterkeys, timekeys, delimitkeys, extrakeys = uri_to_internal(keys)
# drop keys that are not relevant so they don't clutter the URI # drop keys that are not relevant so they don't clutter the URI
if not time: timekeys = {} if not time: timekeys = {}
if not delimit: delimitkeys = {} if not delimit: delimitkeys = {}
if "page" in extrakeys: del extrakeys["page"]
internalkeys = {**filterkeys,**timekeys,**delimitkeys,**extrakeys}
html = "" html = ""
if time:
# all other keys that will not be changed by clicking another filter
#keystr = "?" + compose_querystring(keys,exclude=["since","to","in"])
unchangedkeys = internal_to_uri({**filterkeys,**delimitkeys,**extrakeys})
if time:
# wonky selector for precise date range # wonky selector for precise date range
@ -513,139 +600,78 @@ def module_filterselection(keys,time=True,delimit=False):
# html += "to <input id='dateselect_to' onchange='datechange()' type='date' value='" + "-".join(todate) + "'/>" # html += "to <input id='dateselect_to' onchange='datechange()' type='date' value='" + "-".join(todate) + "'/>"
# html += "</div>" # html += "</div>"
from malojatime import today, thisweek, thismonth, thisyear
### temp!!! this will not allow weekly rank changes
# weekday = ((now.isoweekday()) % 7)
# weekbegin = now - datetime.timedelta(days=weekday)
# weekend = weekbegin + datetime.timedelta(days=6)
# weekbegin = [weekbegin.year,weekbegin.month,weekbegin.day]
# weekend = [weekend.year,weekend.month,weekend.day]
# weekbeginstr = "/".join((str(num) for num in weekbegin))
# weekendstr = "/".join((str(num) for num in weekend))
# relative to current range # relative to current range
html += "<div>" html += "<div>"
# if timekeys.get("timerange").next(-1) is not None:
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(-1)})) + "'><span class='stat_selector'>«</span></a>"
# if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None:
# html += " " + timekeys.get("timerange").desc() + " "
# if timekeys.get("timerange").next(1) is not None:
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(1)})) + "'><span class='stat_selector'>»</span></a>"
if timekeys.get("timerange").next(-1) is not None: thisrange = timekeys.get("timerange")
prevrange = timekeys.get("timerange").next(-1) prevrange = thisrange.next(-1)
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":prevrange})) + "'><span class='stat_selector'>" + prevrange.desc() + "</span></a>" nextrange = thisrange.next(1)
if prevrange is not None:
link = compose_querystring(internal_to_uri({**internalkeys,"timerange":prevrange}))
html += "<a href='?" + link + "'><span class='stat_selector'>" + prevrange.desc() + "</span></a>"
html += " « " html += " « "
if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None: if prevrange is not None or nextrange is not None:
html += "<span class='stat_selector' style='opacity:0.5;'>" + timekeys.get("timerange").desc() + "</span>" html += "<span class='stat_selector' style='opacity:0.5;'>" + thisrange.desc() + "</span>"
if timekeys.get("timerange").next(1) is not None: if nextrange is not None:
html += " » " html += " » "
nextrange = timekeys.get("timerange").next(1) link = compose_querystring(internal_to_uri({**internalkeys,"timerange":nextrange}))
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":nextrange})) + "'><span class='stat_selector'>" + nextrange.desc() + "</span></a>" html += "<a href='?" + link + "'><span class='stat_selector'>" + nextrange.desc() + "</span></a>"
html += "</div>"
# predefined ranges
html += "<div>"
if timekeys.get("timerange") == today():
html += "<span class='stat_selector' style='opacity:0.5;'>Today</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"today"}) + "'><span class='stat_selector'>Today</span></a>"
html += " | "
if timekeys.get("timerange") == thisweek():
html += "<span class='stat_selector' style='opacity:0.5;'>This Week</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"week"}) + "'><span class='stat_selector'>This Week</span></a>"
html += " | "
if timekeys.get("timerange") == thismonth():
html += "<span class='stat_selector' style='opacity:0.5;'>This Month</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"month"}) + "'><span class='stat_selector'>This Month</span></a>"
html += " | "
if timekeys.get("timerange") == thisyear():
html += "<span class='stat_selector' style='opacity:0.5;'>This Year</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"year"}) + "'><span class='stat_selector'>This Year</span></a>"
html += " | "
if timekeys.get("timerange") is None or timekeys.get("timerange").unlimited():
html += "<span class='stat_selector' style='opacity:0.5;'>All Time</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys) + "'><span class='stat_selector'>All Time</span></a>"
html += "</div>"
if delimit:
#keystr = "?" + compose_querystring(keys,exclude=["step","stepn"])
unchangedkeys = internal_to_uri({**filterkeys,**timekeys,**extrakeys})
# only for this element (delimit selector consists of more than one)
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k not in ["step","stepn"]})
html += "<div>"
if delimitkeys.get("step") == "day" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Daily</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"day"}) + "'><span class='stat_selector'>Daily</span></a>"
html += " | "
if delimitkeys.get("step") == "week" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Weekly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"week"}) + "'><span class='stat_selector'>Weekly</span></a>"
html += " | "
if delimitkeys.get("step") == "month" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Monthly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"month"}) + "'><span class='stat_selector'>Monthly</span></a>"
html += " | "
if delimitkeys.get("step") == "year" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Yearly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"year"}) + "'><span class='stat_selector'>Yearly</span></a>"
html += "</div>" html += "</div>"
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k != "trail"})
html += "<div>" categories = [
if delimitkeys.get("trail") == 1: {
html += "<span class='stat_selector' style='opacity:0.5;'>Standard</span>" "active":time,
else: "options":{
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"1"}) + "'><span class='stat_selector'>Standard</span></a>" "Today":{"timerange":today()},
html += " | " "This Week":{"timerange":thisweek()},
"This Month":{"timerange":thismonth()},
"This Year":{"timerange":thisyear()},
"All Time":{"timerange":alltime()}
}
},
{
"active":delimit,
"options":{
"Daily":{"step":"day","stepn":1},
"Weekly":{"step":"week","stepn":1},
"Fortnightly":{"step":"week","stepn":2},
"Monthly":{"step":"month","stepn":1},
"Quarterly":{"step":"month","stepn":3},
"Yearly":{"step":"year","stepn":1}
}
},
{
"active":delimit,
"options":{
"Standard":{"trail":1},
"Trailing":{"trail":2},
"Long Trailing":{"trail":3},
"Inert":{"trail":10},
"Cumulative":{"trail":math.inf}
}
}
if delimitkeys.get("trail") == 2: ]
html += "<span class='stat_selector' style='opacity:0.5;'>Trailing</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"2"}) + "'><span class='stat_selector'>Trailing</span></a>"
html += " | "
if delimitkeys.get("trail") == 3: for c in categories:
html += "<span class='stat_selector' style='opacity:0.5;'>Long Trailing</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"3"}) + "'><span class='stat_selector'>Long Trailing</span></a>"
html += " | "
if delimitkeys.get("trail") == math.inf: if c["active"]:
html += "<span class='stat_selector' style='opacity:0.5;'>Cumulative</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"cumulative":"yes"}) + "'><span class='stat_selector'>Cumulative</span></a>"
html += "</div>" optionlist = []
for option in c["options"]:
values = c["options"][option]
link = "?" + compose_querystring(internal_to_uri({**internalkeys,**values}))
if all(internalkeys.get(k) == values[k] for k in values):
optionlist.append("<span class='stat_selector' style='opacity:0.5;'>" + option + "</span>")
else:
optionlist.append("<a href='" + link + "'><span class='stat_selector'>" + option + "</span></a>")
html += "<div>" + " | ".join(optionlist) + "</div>"
return html return html

22
maloja
View File

@ -183,12 +183,21 @@ def getInstance():
except: except:
return None return None
def getInstanceSupervisor():
try:
output = subprocess.check_output(["pidof","maloja_supervisor"])
pid = int(output)
return pid
except:
return None
def start(): def start():
if install(): if install():
if gotodir(): if gotodir():
setup() setup()
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
p = subprocess.Popen(["python3","supervisor.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
print(green("Maloja started!") + " PID: " + str(p.pid)) print(green("Maloja started!") + " PID: " + str(p.pid))
from doreah import settings from doreah import settings
@ -221,8 +230,12 @@ def restart():
return wasrunning return wasrunning
def stop(): def stop():
pid_sv = getInstanceSupervisor()
if pid_sv is not None:
os.kill(pid_sv,signal.SIGTERM)
pid = getInstance() pid = getInstance()
if pid == None: if pid is None:
print("Server is not running") print("Server is not running")
return False return False
else: else:
@ -283,8 +296,13 @@ def update():
print("Done!") print("Done!")
os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR) os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR)
os.chmod("./update_requirements.sh",os.stat("./update_requirements.sh").st_mode | stat.S_IXUSR)
print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")") try:
returnval = os.system("./update_requirements.sh")
assert returnval == 0
except:
print("Make sure to update required modules! (" + yellow("./update_requirements.sh") + ")")
if stop(): start() #stop returns whether it was running before, in which case we restart it if stop(): start() #stop returns whether it was running before, in which case we restart it

View File

@ -391,6 +391,7 @@ def time_fix(t):
if isinstance(t,MRangeDescriptor): return t if isinstance(t,MRangeDescriptor): return t
if isinstance(t, str): if isinstance(t, str):
if t in ["alltime"]: return None
tod = datetime.datetime.utcnow() tod = datetime.datetime.utcnow()
months = ["january","february","march","april","may","june","july","august","september","october","november","december"] months = ["january","february","march","april","may","june","july","august","september","october","november","december"]
weekdays = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"] weekdays = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]
@ -545,9 +546,8 @@ def time_stamps(since=None,to=None,within=None,range=None):
def delimit_desc(step="month",stepn=1,trail=1): def delimit_desc(step="month",stepn=1,trail=1):
txt = "" txt = ""
if stepn is not 1: txt += _num(stepn) + "-" if stepn is not 1: txt += str(stepn) + "-"
txt += {"year":"Yearly","month":"Monthly","week":"Weekly","day":"Daily"}[step.lower()] txt += {"year":"Yearly","month":"Monthly","week":"Weekly","day":"Daily"}[step.lower()]
#if trail is not 1: txt += " " + _num(trail) + "-Trailing"
if trail is math.inf: txt += " Cumulative" if trail is math.inf: txt += " Cumulative"
elif trail is not 1: txt += " Trailing" #we don't need all the info in the title elif trail is not 1: txt += " Trailing" #we don't need all the info in the title
@ -587,10 +587,11 @@ def ranges(since=None,to=None,within=None,timerange=None,step="month",stepn=1,tr
d_start = d_start.next(stepn-1) #last part of first included range d_start = d_start.next(stepn-1) #last part of first included range
i = 0 i = 0
current_end = d_start current_end = d_start
current_start = current_end.next((stepn*trail-1)*-1)
#ranges = [] #ranges = []
while current_end.first_stamp() <= lastincluded and (max_ is None or i < max_): while current_end.first_stamp() < lastincluded and (max_ is None or i < max_):
current_start = current_end.next((stepn*trail-1)*-1)
if current_start == current_end: if current_start == current_end:
yield current_start yield current_start
#ranges.append(current_start) #ranges.append(current_start)
@ -598,6 +599,7 @@ def ranges(since=None,to=None,within=None,timerange=None,step="month",stepn=1,tr
yield MRange(current_start,current_end) yield MRange(current_start,current_end)
#ranges.append(MRange(current_start,current_end)) #ranges.append(MRange(current_start,current_end))
current_end = current_end.next(stepn) current_end = current_end.next(stepn)
current_start = current_end.next((stepn*trail-1)*-1)
i += 1 i += 1
@ -619,6 +621,8 @@ def thismonth():
def thisyear(): def thisyear():
tod = datetime.datetime.utcnow() tod = datetime.datetime.utcnow()
return MTime(tod.year) return MTime(tod.year)
def alltime():
return MRange(None,None)
#def _get_start_of(timestamp,unit): #def _get_start_of(timestamp,unit):
# date = datetime.datetime.utcfromtimestamp(timestamp) # date = datetime.datetime.utcfromtimestamp(timestamp)

Binary file not shown.

View File

@ -1,6 +1,7 @@
bottle>=0.12.16 bottle>=0.12.16
waitress>=1.3 waitress>=1.3
doreah>=0.9.1 doreah>=1.1.7
nimrodel>=0.4.9 nimrodel>=0.4.9
setproctitle>=1.1.10 setproctitle>=1.1.10
wand>=0.5.4 wand>=0.5.4
lesscpy>=0.13

View File

@ -10,4 +10,5 @@ countas S Club 7 Tina Barrett
countas RenoakRhythm Approaching Nirvana countas RenoakRhythm Approaching Nirvana
countas Shirley Manson Garbage countas Shirley Manson Garbage
countas Lewis Brindley The Yogscast countas Lewis Brindley The Yogscast
countas Sips The Yogscast countas Sips The Yogscast
countas Sjin The Yogscast

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -16,6 +16,7 @@ replacetitle Cause I'm God Girl Roll Deep
countas 2Yoon 4Minute countas 2Yoon 4Minute
replaceartist 4minute 4Minute replaceartist 4minute 4Minute
replacetitle 미쳐 Crazy replacetitle 미쳐 Crazy
addartists HyunA Change Jun Hyung
# BLACKPINK # BLACKPINK
countas Jennie BLACKPINK countas Jennie BLACKPINK
@ -47,8 +48,8 @@ replacetitle 나비 (Butterfly) Butterfly
replacetitle Déjà vu Déjà Vu replacetitle Déjà vu Déjà Vu
replacetitle 라차타 (LA chA TA) LA chA TA replacetitle 라차타 (LA chA TA) LA chA TA
replacetitle 여우 같은 내 친구 (No More) No More replacetitle 여우 같은 내 친구 (No More) No More
replacetitle 시그널 (Signal) Signal replacetitle 시그널 (Signal) Signal
replacetitle 미행 (그림자 : Shadow) Shadow replacetitle 미행 (그림자 : Shadow) Shadow
# Stellar # Stellar
replaceartist STELLAR Stellar replaceartist STELLAR Stellar
@ -58,6 +59,7 @@ replacetitle 찔려 Sting Sting
# Red Velvet # Red Velvet
countas Seulgi Red Velvet countas Seulgi Red Velvet
countas Joy Red Velvet
replacetitle 러시안 룰렛 Russian Roulette Russian Roulette replacetitle 러시안 룰렛 Russian Roulette Russian Roulette
replacetitle 피카부 Peek-a-Boo Peek-A-Boo replacetitle 피카부 Peek-a-Boo Peek-A-Boo
replacetitle 빨간 맛 Red Flavor Red Flavor replacetitle 빨간 맛 Red Flavor Red Flavor
@ -81,6 +83,7 @@ replacetitle CHEER UP Cheer Up
replacetitle OOH-AHH하게 Like OOH-AHH Like Ooh-Ahh replacetitle OOH-AHH하게 Like OOH-AHH Like Ooh-Ahh
replacetitle OOH-AHH Like Ooh-Ahh replacetitle OOH-AHH Like Ooh-Ahh
replacetitle LIKEY Likey replacetitle LIKEY Likey
countas Tzuyu TWICE
# AOA # AOA
countas AOA Black AOA countas AOA Black AOA
@ -145,5 +148,8 @@ replaceartist A pink Apink
# Chungha & IOI # Chungha & IOI
replaceartist CHUNG HA Chungha replaceartist CHUNG HA Chungha
replaceartist 청하 CHUNGHA Chungha replaceartist 청하 CHUNGHA Chungha
#countas Chungha I.O.I # Chungha is too famous #countas Chungha I.O.I # Chungha is too famous
replacetitle 벌써 12시 Gotta Go Gotta Go replacetitle 벌써 12시 Gotta Go Gotta Go
# ITZY
replacetitle 달라달라 (DALLA DALLA) Dalla Dalla

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -16,6 +16,10 @@ The first column defines the type of the rule:
This will not change the separation in the database and all effects of this rule will disappear as soon as it is no longer active. This will not change the separation in the database and all effects of this rule will disappear as soon as it is no longer active.
Second column is the artist Second column is the artist
Third column the replacement artist / grouping label Third column the replacement artist / grouping label
addartists Defines a certain combination of artists and song title that should always have other artists added.
Second column is artists that need to be already present for this rule to apply
Third column is the song title
Fourth column are artists that shoud be added, separated by ␟
Rules in non-tsv files are ignored. '#' is used for comments. Additional columns are ignored. To have a '#' in a name, use '\num' Rules in non-tsv files are ignored. '#' is used for comments. Additional columns are ignored. To have a '#' in a name, use '\num'
Comments are not supported in scrobble lists, but you probably never edit these manually anyway. Comments are not supported in scrobble lists, but you probably never edit these manually anyway.
@ -30,3 +34,4 @@ replacetitle 첫 사랑니 (Rum Pum Pum Pum) Rum Pum Pum Pum
replaceartist Dal Shabet Dal★Shabet replaceartist Dal Shabet Dal★Shabet
replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 # one artist is replaced by two artists replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 # one artist is replaced by two artists
countas Trouble Maker HyunA countas Trouble Maker HyunA
addartists HyunA Change Jun Hyung

View File

@ -28,6 +28,18 @@ pages = {
"https://open.spotify.com" "https://open.spotify.com"
], ],
"script":"spotify.js" "script":"spotify.js"
},
"Bandcamp":{
"patterns":[
"bandcamp.com"
],
"script":"bandcamp.js"
},
"Soundcloud":{
"patterns":[
"https://soundcloud.com"
],
"script":"soundcloud.js"
} }
} }
@ -51,7 +63,7 @@ function onTabUpdated(tabId, changeInfo, tab) {
patterns = pages[page]["patterns"]; patterns = pages[page]["patterns"];
//console.log("Page was managed by a " + page + " manager") //console.log("Page was managed by a " + page + " manager")
for (var i=0;i<patterns.length;i++) { for (var i=0;i<patterns.length;i++) {
if (tab.url.startsWith(patterns[i])) { if (tab.url.includes(patterns[i])) {
//console.log("Still on same page!") //console.log("Still on same page!")
tabManagers[tabId].update(); tabManagers[tabId].update();
@ -67,7 +79,7 @@ function onTabUpdated(tabId, changeInfo, tab) {
if (pages.hasOwnProperty(key)) { if (pages.hasOwnProperty(key)) {
patterns = pages[key]["patterns"]; patterns = pages[key]["patterns"];
for (var i=0;i<patterns.length;i++) { for (var i=0;i<patterns.length;i++) {
if (tab.url.startsWith(patterns[i])) { if (tab.url.includes(patterns[i])) {
console.log("New page on tab " + tabId + " will be handled by new " + key + " manager!"); console.log("New page on tab " + tabId + " will be handled by new " + key + " manager!");
tabManagers[tabId] = new Controller(tabId,key); tabManagers[tabId] = new Controller(tabId,key);
updateTabNum(); updateTabNum();
@ -166,8 +178,13 @@ class Controller {
actuallyupdate() { actuallyupdate() {
this.messageID++; this.messageID++;
//console.log("Update! Our page is " + this.page + ", our tab id " + this.tabId) //console.log("Update! Our page is " + this.page + ", our tab id " + this.tabId)
chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]}); try {
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"}); chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]});
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"});
}
catch (e) {
console.log("Could not run site script. Tab probably closed or something idk.")
}
this.alreadyQueued = false; this.alreadyQueued = false;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "Maloja Scrobbler", "name": "Maloja Scrobbler",
"version": "1.3", "version": "1.4",
"description": "Scrobbles tracks from various sites to your Maloja server", "description": "Scrobbles tracks from various sites to your Maloja server",
"manifest_version": 2, "manifest_version": 2,
"permissions": ["activeTab", "permissions": ["activeTab",

View File

@ -0,0 +1,15 @@
maloja_scrobbler_selector_playbar = "//div[contains(@class,'trackView')]"
maloja_scrobbler_selector_metadata = "."
// need to select everything as bar / metadata block because artist isn't shown in the inline player
maloja_scrobbler_selector_title = ".//span[@class='title']/text()"
maloja_scrobbler_selector_artist = ".//span[contains(@itemprop,'byArtist')]/a/text()"
maloja_scrobbler_selector_duration = ".//span[@class='time_total']/text()"
maloja_scrobbler_selector_control = ".//td[@class='play_cell']/a[@role='button']/div[contains(@class,'playbutton')]/@class"
maloja_scrobbler_label_playing = "playbutton playing"
maloja_scrobbler_label_paused = "playbutton"

View File

@ -0,0 +1,14 @@
maloja_scrobbler_selector_playbar = "//div[contains(@class,'playControls')]"
maloja_scrobbler_selector_metadata = ".//div[contains(@class,'playControls__soundBadge')]//div[contains(@class,'playbackSoundBadge__titleContextContainer')]"
maloja_scrobbler_selector_title = ".//div/a/@title"
maloja_scrobbler_selector_artist = ".//a/text()"
maloja_scrobbler_selector_duration = ".//div[contains(@class,'playbackTimeline__duration')]//span[@aria-hidden='true']/text()"
maloja_scrobbler_selector_control = ".//button[contains(@class,'playControl')]/@title"
maloja_scrobbler_label_playing = "Pause current"
maloja_scrobbler_label_paused = "Play current"

View File

@ -9,4 +9,4 @@ maloja_scrobbler_selector_artist = "./text()"
maloja_scrobbler_selector_duration = ".//div[@class='playback-bar__progress-time'][2]/text()" maloja_scrobbler_selector_duration = ".//div[@class='playback-bar__progress-time'][2]/text()"
maloja_scrobbler_selector_control = ".//div[contains(@class,'player-controls__buttons')]/button[3]/@title" maloja_scrobbler_selector_control = ".//div[contains(@class,'player-controls__buttons')]/div[3]/button/@title"

View File

@ -65,12 +65,24 @@ else {
control = bar.xpath(maloja_scrobbler_selector_control, XPathResult.STRING_TYPE); control = bar.xpath(maloja_scrobbler_selector_control, XPathResult.STRING_TYPE);
if (control == "Play") { try {
label_playing = maloja_scrobbler_label_playing
}
catch {
label_playing = "Pause"
}
try {
label_paused = maloja_scrobbler_label_paused
}
catch {
label_paused = "Play"
}
if (control == label_paused) {
console.log("Not playing right now"); console.log("Not playing right now");
chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:artist,title:title}); chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:artist,title:title});
//stopPlayback() //stopPlayback()
} }
else if (control == "Pause") { else if (control == label_playing) {
console.log("Playing " + artist + " - " + title + " (" + durationSeconds + " sec)"); console.log("Playing " + artist + " - " + title + " (" + durationSeconds + " sec)");
chrome.runtime.sendMessage({type:"startPlayback",time:Math.floor(Date.now()),artist:artist,title:title,duration:durationSeconds}); chrome.runtime.sendMessage({type:"startPlayback",time:Math.floor(Date.now()),artist:artist,title:title,duration:durationSeconds});
//startPlayback(artist,title,durationSeconds) //startPlayback(artist,title,durationSeconds)

View File

@ -1,17 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
# server stuff # server stuff
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template, HTTPResponse
import waitress import waitress
# monkey patching # monkey patching
import monkey import monkey
# rest of the project # rest of the project
import database import database
import utilities
import htmlmodules import htmlmodules
import htmlgenerators import htmlgenerators
import malojatime import malojatime
from utilities import * import utilities
from utilities import resolveImage
from urihandler import uri_to_internal, remove_identical from urihandler import uri_to_internal, remove_identical
import urihandler import urihandler
# doreah toolkit # doreah toolkit
@ -26,20 +26,28 @@ import os
import setproctitle import setproctitle
# url handling # url handling
import urllib import urllib
import urllib.request
import urllib.parse
from urllib.error import *
#settings.config(files=["settings/default.ini","settings/settings.ini"]) #settings.config(files=["settings/default.ini","settings/settings.ini"])
#settings.update("settings/default.ini","settings/settings.ini") #settings.update("settings/default.ini","settings/settings.ini")
MAIN_PORT = settings.get_settings("WEB_PORT") MAIN_PORT = settings.get_settings("WEB_PORT")
HOST = settings.get_settings("HOST")
webserver = Bottle() webserver = Bottle()
import lesscpy
css = ""
for f in os.listdir("website/less"):
css += lesscpy.compile("website/less/" + f)
os.makedirs("website/css",exist_ok=True)
with open("website/css/style.css","w") as f:
f.write(css)
@webserver.route("") @webserver.route("")
@webserver.route("/") @webserver.route("/")
def mainpage(): def mainpage():
@ -77,7 +85,11 @@ def customerror(error):
def graceful_exit(sig=None,frame=None): def graceful_exit(sig=None,frame=None):
#urllib.request.urlopen("http://[::1]:" + str(DATABASE_PORT) + "/sync") #urllib.request.urlopen("http://[::1]:" + str(DATABASE_PORT) + "/sync")
database.sync() log("Received signal to shutdown")
try:
database.sync()
except Exception as e:
log("Error while shutting down!",e)
log("Server shutting down...") log("Server shutting down...")
os._exit(42) os._exit(42)
@ -121,6 +133,7 @@ def static_image(pth):
#@webserver.route("/<name:re:.*\\.html>") #@webserver.route("/<name:re:.*\\.html>")
@webserver.route("/<name:re:.*\\.js>") @webserver.route("/<name:re:.*\\.js>")
@webserver.route("/<name:re:.*\\.css>") @webserver.route("/<name:re:.*\\.css>")
@webserver.route("/<name:re:.*\\.less>")
@webserver.route("/<name:re:.*\\.png>") @webserver.route("/<name:re:.*\\.png>")
@webserver.route("/<name:re:.*\\.jpeg>") @webserver.route("/<name:re:.*\\.jpeg>")
@webserver.route("/<name:re:.*\\.ico>") @webserver.route("/<name:re:.*\\.ico>")
@ -132,7 +145,7 @@ def static(name):
@webserver.route("/<name>") @webserver.route("/<name>")
def static_html(name): def static_html(name):
linkheaders = ["</css/maloja.css>; rel=preload; as=style"] linkheaders = ["</css/style.css>; rel=preload; as=style"]
keys = remove_identical(FormsDict.decode(request.query)) keys = remove_identical(FormsDict.decode(request.query))
# if a pyhp file exists, use this # if a pyhp file exists, use this
@ -206,6 +219,16 @@ def static_html(name):
return html return html
#return static_file("website/" + name + ".html",root="") #return static_file("website/" + name + ".html",root="")
# Shortlinks
@webserver.get("/artist/<artist>")
def redirect_artist(artist):
redirect("/artist?artist=" + artist)
@webserver.get("/track/<artists:path>/<title>")
def redirect_track(artists,title):
redirect("/track?title=" + title + "&" + "&".join("artist=" + artist for artist in artists.split("/")))
#set graceful shutdown #set graceful shutdown
signal.signal(signal.SIGINT, graceful_exit) signal.signal(signal.SIGINT, graceful_exit)
signal.signal(signal.SIGTERM, graceful_exit) signal.signal(signal.SIGTERM, graceful_exit)
@ -215,8 +238,7 @@ setproctitle.setproctitle("Maloja")
## start database ## start database
database.start_db() database.start_db()
#database.register_subroutes(webserver,"/api")
database.dbserver.mount(server=webserver) database.dbserver.mount(server=webserver)
log("Starting up Maloja server...") log("Starting up Maloja server...")
run(webserver, host='::', port=MAIN_PORT, server='waitress') run(webserver, host=HOST, port=MAIN_PORT, server='waitress')

View File

@ -1,19 +1,30 @@
# Do not change settings in this file
# Instead, simply write an entry with the same name in your own settings.ini file
# Category headers in [brackets] are only for organization and not necessary
[HTTP] [HTTP]
WEB_PORT = 42010 WEB_PORT = 42010
HOST = "::" # You most likely want either :: for IPv6 or 0.0.0.0 for IPv4 here
[Third Party Services] [Third Party Services]
LASTFM_API_KEY = "ASK" # 'ASK' signifies that the user has not yet indicated to not use any key at all. LASTFM_API_KEY = "ASK" # 'ASK' signifies that the user has not yet indicated to not use any key at all.
LASTFM_API_SECRET = "ASK"
FANARTTV_API_KEY = "ASK" FANARTTV_API_KEY = "ASK"
SPOTIFY_API_ID = "ASK" SPOTIFY_API_ID = "ASK"
SPOTIFY_API_SECRET = "ASK" SPOTIFY_API_SECRET = "ASK"
CACHE_EXPIRE_NEGATIVE = 30 # after how many days negative results should be tried again CACHE_EXPIRE_NEGATIVE = 30 # after how many days negative results should be tried again
CACHE_EXPIRE_POSITIVE = 300 # after how many days positive results should be refreshed CACHE_EXPIRE_POSITIVE = 300 # after how many days positive results should be refreshed
# Can be 'YouTube', 'YouTube Music', 'Google Play Music', 'Spotify', 'Tidal', 'SoundCloud', 'Deezer', 'Amazon Music'
# Omit or set to none to disable
TRACK_SEARCH_PROVIDER = None
[Database] [Database]
DB_CACHE_SIZE = 8192 # how many MB on disk each database cache should have available. DB_CACHE_SIZE = 8192 # how many MB on disk each database cache should have available.
INVALID_ARTISTS = ["[Unknown Artist]","Unknown Artist"]
[Local Images] [Local Images]
@ -27,8 +38,11 @@ LOCAL_IMAGE_ROTATE = 3600 # when multiple images are present locally, how many s
DEFAULT_RANGE_CHARTS_ARTISTS = year DEFAULT_RANGE_CHARTS_ARTISTS = year
DEFAULT_RANGE_CHARTS_TRACKS = year DEFAULT_RANGE_CHARTS_TRACKS = year
# same for pulse view # same for pulse view
# can be days, weeks, months, years # can be day, week, month, year
DEFAULT_RANGE_PULSE = months DEFAULT_STEP_PULSE = month
# display top tiles on artist and track chart pages
CHARTS_DISPLAY_TILES = false
[Fluff] [Fluff]
@ -36,6 +50,8 @@ DEFAULT_RANGE_PULSE = months
SCROBBLES_GOLD = 250 SCROBBLES_GOLD = 250
SCROBBLES_PLATINUM = 500 SCROBBLES_PLATINUM = 500
SCROBBLES_DIAMOND = 1000 SCROBBLES_DIAMOND = 1000
# name for comparisons
NAME = "Generic Maloja User"
[Misc] [Misc]

25
supervisor.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import subprocess
import time
import setproctitle
import signal
from doreah.logging import log
setproctitle.setproctitle("maloja_supervisor")
while True:
time.sleep(60)
try:
output = subprocess.check_output(["pidof","Maloja"])
pid = int(output)
log("Maloja is running, PID " + str(pid),module="supervisor")
except:
log("Maloja is not running, restarting...",module="supervisor")
try:
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
except e:
log("Error starting Maloja: " + str(e),module="supervisor")

2
update_requirements.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
pip3 install -r requirements.txt --upgrade --no-cache-dir

View File

@ -103,8 +103,12 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False):
#4 #4
resultkeys4 = {"max_":300} resultkeys4 = {"page":0,"perpage":100}
if "max" in keys: resultkeys4["max_"] = int(keys["max"]) # if "max" in keys: resultkeys4["max_"] = int(keys["max"])
if "max" in keys: resultkeys4["page"],resultkeys4["perpage"] = 0, int(keys["max"])
#different max than the internal one! the user doesn't get to disable pagination
if "page" in keys: resultkeys4["page"] = int(keys["page"])
if "perpage" in keys: resultkeys4["perpage"] = int(keys["perpage"])
return resultkeys1, resultkeys2, resultkeys3, resultkeys4 return resultkeys1, resultkeys2, resultkeys3, resultkeys4
@ -146,8 +150,12 @@ def internal_to_uri(keys):
urikeys.append("trail",str(keys["trail"])) urikeys.append("trail",str(keys["trail"]))
# stuff # stuff
if "max_" in keys: #if "max_" in keys:
urikeys.append("max",str(keys["max_"])) # urikeys.append("max",str(keys["max_"]))
if "page" in keys:
urikeys.append("page",str(keys["page"]))
if "perpage" in keys:
urikeys.append("perpage",str(keys["perpage"]))
return urikeys return urikeys

View File

@ -99,14 +99,11 @@ def consistentRulestate(folder,checksums):
if (scrobblefile.endswith(".tsv")): if (scrobblefile.endswith(".tsv")):
try: try:
f = open(folder + "/" + scrobblefile + ".rulestate","r") with open(folder + "/" + scrobblefile + ".rulestate","r") as f:
if f.read() != checksums: if f.read() != checksums:
return False return False
except: except:
return False return False
finally:
f.close()
return True return True
@ -442,8 +439,12 @@ def update_medals():
from database import MEDALS, MEDALS_TRACKS, STAMPS, get_charts_artists, get_charts_tracks from database import MEDALS, MEDALS_TRACKS, STAMPS, get_charts_artists, get_charts_tracks
firstyear = datetime.datetime.utcfromtimestamp(STAMPS[0]).year
currentyear = datetime.datetime.utcnow().year currentyear = datetime.datetime.utcnow().year
try:
firstyear = datetime.datetime.utcfromtimestamp(STAMPS[0]).year
except:
firstyear = currentyear
MEDALS.clear() MEDALS.clear()
for year in range(firstyear,currentyear): for year in range(firstyear,currentyear):
@ -468,3 +469,23 @@ def update_medals():
elif t["rank"] == 2: MEDALS_TRACKS.setdefault(track,{}).setdefault("silver",[]).append(year) elif t["rank"] == 2: MEDALS_TRACKS.setdefault(track,{}).setdefault("silver",[]).append(year)
elif t["rank"] == 3: MEDALS_TRACKS.setdefault(track,{}).setdefault("bronze",[]).append(year) elif t["rank"] == 3: MEDALS_TRACKS.setdefault(track,{}).setdefault("bronze",[]).append(year)
else: break else: break
@daily
def update_weekly():
from database import WEEKLY_TOPTRACKS, WEEKLY_TOPARTISTS, get_charts_artists, get_charts_tracks
from malojatime import ranges, thisweek
WEEKLY_TOPARTISTS.clear()
WEEKLY_TOPTRACKS.clear()
for week in ranges(step="week"):
if week == thisweek(): break
for a in get_charts_artists(timerange=week):
artist = a["artist"]
if a["rank"] == 1: WEEKLY_TOPARTISTS[artist] = WEEKLY_TOPARTISTS.setdefault(artist,0) + 1
for t in get_charts_tracks(timerange=week):
track = (frozenset(t["track"]["artists"]),t["track"]["title"])
if t["rank"] == 1: WEEKLY_TOPTRACKS[track] = WEEKLY_TOPTRACKS.setdefault(track,0) + 1

View File

@ -4,7 +4,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Maloja - KEY_ARTISTNAME</title> <title>Maloja - KEY_ARTISTNAME</title>
<script src="javascript/rangeselect.js" async></script> <script src="javascript/cookies.js"></script>
<script src="javascript/rangeselect.js"></script>
</head> </head>
<body> <body>
@ -15,13 +16,13 @@
</td> </td>
<td class="text"> <td class="text">
<h1>KEY_ARTISTNAME</h1> <h1>KEY_ARTISTNAME</h1>
<span class="rank"><a href="/charts_artists?max=100">KEY_POSITION</a></span> <span class="rank"><a href="/charts_artists">KEY_POSITION</a></span>
<br/> <br/>
<span>KEY_ASSOCIATED</span> <span>KEY_ASSOCIATED</span>
<p class="stats"><a href="/scrobbles?artist=KEY_ENC_ARTISTNAME">KEY_SCROBBLES Scrobbles</a></p> <p class="stats"><a href="/scrobbles?artist=KEY_ENC_ARTISTNAME">KEY_SCROBBLES Scrobbles</a></p>
<p class="desc">KEY_DESCRIPTION</p> <p class="desc">KEY_DESCRIPTION</p>
<span>KEY_MEDALS</span> <span>KEY_MEDALS</span> <span>KEY_TOPWEEKS</span> <span>KEY_CERTS</span>
</td> </td>
</tr> </tr>
</table> </table>
@ -32,35 +33,35 @@
<table class="twopart"> <table class="twopart">
<tr> <tr>
<td> <td>
<h2><a href='/pulse?artist=KEY_ENC_ARTISTNAME&step=year&trail=1'>Pulse</a></h2> <h2><a class="stat_link_pulse" href='/pulse?artist=KEY_ENC_ARTISTNAME&trail=1&step=month'>Pulse</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span> <span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span> | <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span> | <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span> | <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/> <br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span> <span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span> <span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span> <span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
</td> </td>
<td> <td>
<!-- We use the same classes / function calls here because we want it to switch together with pulse --> <!-- We use the same classes / function calls here because we want it to switch together with pulse -->
<h2><a href='/performance?artist=KEY_ENC_CREDITEDARTISTNAME&step=year&trail=1'>Performance</a></h2> <h2><a class="stat_link_pulse" href='/performance?artist=KEY_ENC_CREDITEDARTISTNAME&trail=1&step=month'>Performance</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span> <span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span> | <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span> | <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span> | <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/> <br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PERFORMANCE_MONTHS</span> <span class="stat_module_pulse pulse_month">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PERFORMANCE_DAYS</span> <span class="stat_module_pulse pulse_day" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PERFORMANCE_YEARS</span> <span class="stat_module_pulse pulse_year" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PERFORMANCE_WEEKS</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -5,7 +5,7 @@ from malojatime import today,thisweek,thismonth,thisyear
def instructions(keys): def instructions(keys):
from utilities import getArtistImage from utilities import getArtistImage
from htmlgenerators import artistLink, artistLinks from htmlgenerators import artistLink, artistLinks, link_address
from urihandler import compose_querystring, uri_to_internal from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_pulse, module_performance, module_trackcharts, module_scrobblelist from htmlmodules import module_pulse, module_performance, module_trackcharts, module_scrobblelist
@ -22,13 +22,31 @@ def instructions(keys):
if "medals" in data and data["medals"] is not None: if "medals" in data and data["medals"] is not None:
if "gold" in data["medals"]: if "gold" in data["medals"]:
for y in data["medals"]["gold"]: for y in data["medals"]["gold"]:
html_medals += "<a title='Best Artist in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Best Artist in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "silver" in data["medals"]: if "silver" in data["medals"]:
for y in data["medals"]["silver"]: for y in data["medals"]["silver"]:
html_medals += "<a title='Second Best Artist in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Second Best Artist in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "bronze" in data["medals"]: if "bronze" in data["medals"]:
for y in data["medals"]["bronze"]: for y in data["medals"]["bronze"]:
html_medals += "<a title='Third Best Artist in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Third Best Artist in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_cert = ""
for track in database.get_tracks(artist=artist):
info = database.trackInfo(track)
if info.get("certification") is not None:
img = "/media/record_{cert}.png".format(cert=info["certification"])
trackname = track["title"].replace("'","&#39;")
tracklink = link_address(track)
tooltip = "{title} has reached {cert} status".format(title=trackname,cert=info["certification"].capitalize())
html_cert += "<a href='{link}'><img class='certrecord_small' src='{img}' title='{tooltip}' /></a>".format(tooltip=tooltip,img=img,link=tracklink)
html_topweeks = ""
if data.get("topweeks") not in [0,None]:
link = "/performance?artist=" + urllib.parse.quote(keys["artist"]) + "&trail=1&step=week"
title = str(data["topweeks"]) + " weeks on #1"
html_topweeks = "<a title='" + title + "' href='" + link + "'><img class='star' src='/media/star.png' />" + str(data["topweeks"]) + "</a>"
credited = data.get("replace") credited = data.get("replace")
includestr = " " includestr = " "
@ -69,6 +87,8 @@ def instructions(keys):
"KEY_POSITION":pos, "KEY_POSITION":pos,
"KEY_ASSOCIATED":includestr, "KEY_ASSOCIATED":includestr,
"KEY_MEDALS":html_medals, "KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert,
"KEY_TOPWEEKS":html_topweeks,
# tracks # tracks
"KEY_TRACKLIST":html_tracks, "KEY_TRACKLIST":html_tracks,
# pulse # pulse

View File

@ -11,7 +11,7 @@
<table class="top_info"> <table class="top_info">
<tr> <tr>
<td class="image"> <td class="image">
<div style="background-image:url('KEY_TOPARTIST_IMAGEURL')"></div> KEY_TOPARTIST_IMAGEDIV
</td> </td>
<td class="text"> <td class="text">
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/> <h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
@ -24,6 +24,9 @@
</tr> </tr>
</table> </table>
<span class="stat_module_topartists">
KEY_ARTISTCHART
</span>
KEY_ARTISTLIST KEY_ARTISTLIST

View File

@ -4,8 +4,9 @@ import urllib
def instructions(keys): def instructions(keys):
from utilities import getArtistImage from utilities import getArtistImage
from urihandler import compose_querystring, uri_to_internal from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_artistcharts, module_filterselection from htmlmodules import module_artistcharts, module_filterselection, module_artistcharts_tiles
from malojatime import range_desc from malojatime import range_desc
from doreah.settings import get_settings
_, timekeys, _, amountkeys = uri_to_internal(keys) _, timekeys, _, amountkeys = uri_to_internal(keys)
@ -16,6 +17,7 @@ def instructions(keys):
html_charts, rep = module_artistcharts(**amountkeys,**timekeys) html_charts, rep = module_artistcharts(**amountkeys,**timekeys)
if rep is not None: if rep is not None:
@ -23,12 +25,23 @@ def instructions(keys):
else: else:
imgurl = "" imgurl = ""
html_tiles = ""
if get_settings("CHARTS_DISPLAY_TILES"):
html_tiles = module_artistcharts_tiles(timerange=timekeys["timerange"])
imgurl = "favicon.png"
imgdiv = '<div style="background-image:url('+imgurl+')"></div>'
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else [] pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
replace = {"KEY_TOPARTIST_IMAGEURL":imgurl, replace = {
"KEY_ARTISTLIST":html_charts, "KEY_TOPARTIST_IMAGEDIV":imgdiv,
"KEY_RANGE":limitstring, "KEY_ARTISTCHART":html_tiles,
"KEY_FILTERSELECTOR":html_filterselector} "KEY_ARTISTLIST":html_charts,
"KEY_RANGE":limitstring,
"KEY_FILTERSELECTOR":html_filterselector
}
return (replace,pushresources) return (replace,pushresources)

View File

@ -10,7 +10,7 @@
<table class="top_info"> <table class="top_info">
<tr> <tr>
<td class="image"> <td class="image">
<div style="background-image:url('KEY_TOPARTIST_IMAGEURL')"></div> KEY_TOPARTIST_IMAGEDIV
</td> </td>
<td class="text"> <td class="text">
<h1>Track Charts</h1>TOP_TRACKS_LINK<br/> <h1>Track Charts</h1>TOP_TRACKS_LINK<br/>
@ -22,6 +22,9 @@
</tr> </tr>
</table> </table>
<span class="stat_module_topartists">
KEY_TRACKCHART
</span>
KEY_TRACKLIST KEY_TRACKLIST

View File

@ -5,8 +5,9 @@ def instructions(keys):
from utilities import getArtistImage, getTrackImage from utilities import getArtistImage, getTrackImage
from htmlgenerators import artistLink from htmlgenerators import artistLink
from urihandler import compose_querystring, uri_to_internal from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_trackcharts, module_filterselection from htmlmodules import module_trackcharts, module_filterselection, module_trackcharts_tiles
from malojatime import range_desc from malojatime import range_desc
from doreah.settings import get_settings
filterkeys, timekeys, _, amountkeys = uri_to_internal(keys) filterkeys, timekeys, _, amountkeys = uri_to_internal(keys)
@ -23,6 +24,9 @@ def instructions(keys):
html_charts, rep = module_trackcharts(**amountkeys,**timekeys,**filterkeys) html_charts, rep = module_trackcharts(**amountkeys,**timekeys,**filterkeys)
html_tiles = ""
if filterkeys.get("artist") is not None: if filterkeys.get("artist") is not None:
imgurl = getArtistImage(filterkeys.get("artist")) imgurl = getArtistImage(filterkeys.get("artist"))
limitstring = "by " + artistLink(filterkeys.get("artist")) limitstring = "by " + artistLink(filterkeys.get("artist"))
@ -31,6 +35,15 @@ def instructions(keys):
else: else:
imgurl = "" imgurl = ""
html_tiles = ""
if get_settings("CHARTS_DISPLAY_TILES"):
html_tiles = module_trackcharts_tiles(timerange=timekeys["timerange"])
imgurl = "favicon.png"
imgdiv = '<div style="background-image:url('+imgurl+')"></div>'
limitstring += " " + timekeys["timerange"].desc(prefix=True) limitstring += " " + timekeys["timerange"].desc(prefix=True)
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else [] pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
@ -38,7 +51,8 @@ def instructions(keys):
replace = { replace = {
"KEY_TOPARTIST_IMAGEURL":imgurl, "KEY_TOPARTIST_IMAGEDIV":imgdiv,
"KEY_TRACKCHART":html_tiles,
"KEY_TRACKLIST":html_charts, "KEY_TRACKLIST":html_charts,
"KEY_LIMITS":limitstring, "KEY_LIMITS":limitstring,
"KEY_FILTERSELECTOR":html_filterselector, "KEY_FILTERSELECTOR":html_filterselector,

View File

@ -1,3 +1,9 @@
<meta name="description" content='Maloja is a self-hosted music scrobble server.' /> <meta name="description" content='Maloja is a self-hosted music scrobble server.' />
<!--<link rel="stylesheet/less" href="/less/maloja.less" />
<link rel="stylesheet/less" href="/less/grisons.less" />
<link rel="stylesheet" href="/css/maloja.css" /> <link rel="stylesheet" href="/css/maloja.css" />
<script src="/javascript/search.js" async="yes"></script> <link rel="stylesheet" href="/css/grisons.css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/3.9.0/less.min.js" ></script> -->
<link rel="stylesheet" href="/css/style.css" />
<script src="/javascript/search.js" async></script>

78
website/compare.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Maloja - Compare</title>
<style>
.comparecircle {
height:500px;
width:500px;
border-radius:250px;
border: 1px solid rgba(245,245,220,0.3);
margin:auto;
margin-top:100px;
text-align:center;
line-height:500px;
font-size:60px;
color:black;
/* background-image: linear-gradient(to right,KEY_CIRCLE_CSS); */
background-image: radial-gradient(#KEY_CICLE_COLOR KEY_FULLMATCHpx, transparent KEY_PARTIALMATCHpx);
}
table tr td:first-child {
text-align: left;
padding:10px;
width:33%;
}
table tr td {
text-align: center;
padding:10px;
}
table tr td:last-child {
text-align: right;
padding:10px;
width:33%;
}
</style>
</head>
<body>
<table style="width:99%;">
<tr>
<td><h1>KEY_NAME_SELF</h1></td>
<td>
<div class="comparecircle">
KEY_MATCH%
</div>
</td>
<td><h1>KEY_NAME_OTHER</h1></td>
</tr>
<tr>
<td></td>
<td style="font-size:70%;color:grey;">
The size of the circle shows matching music taste.
The fuzziness of its border indicates differences in quantity.
</td>
<td></td>
</tr>
<tr>
<td></td>
<td>
<span>Common Favorite</span>
<h2 style="margin:7px;">KEY_BESTARTIST_LINK</h2>
<img src="KEY_BESTARTIST_IMAGE" style="width:80px;" />
</td>
<td></td>
</tr>
</table>
</body>
</html>

88
website/compare.py Normal file
View File

@ -0,0 +1,88 @@
import urllib
import database
import json
from htmlgenerators import artistLink
from utilities import getArtistImage
def instructions(keys):
compareto = keys.get("to")
compareurl = compareto + "/api/info"
response = urllib.request.urlopen(compareurl)
strangerinfo = json.loads(response.read())
owninfo = database.info()
artists = {}
for a in owninfo["artists"]:
artists[a.lower()] = {"name":a,"self":int(owninfo["artists"][a]*1000),"other":0}
for a in strangerinfo["artists"]:
artists[a.lower()] = artists.setdefault(a.lower(),{"name":a,"self":0})
artists[a.lower()]["other"] = int(strangerinfo["artists"][a]*1000)
for a in artists:
common = min(artists[a]["self"],artists[a]["other"])
artists[a]["self"] -= common
artists[a]["other"] -= common
artists[a]["common"] = common
best = sorted((artists[a]["name"] for a in artists),key=lambda x: artists[x.lower()]["common"],reverse=True)
result = {
"unique_self":sum(artists[a]["self"] for a in artists if artists[a]["common"] == 0),
"more_self":sum(artists[a]["self"] for a in artists if artists[a]["common"] != 0),
# "common":{
# **{
# artists[a]["name"]:artists[a]["common"]
# for a in best[:3]},
# None: sum(artists[a]["common"] for a in artists if a not in best[:3])
# },
"common":sum(artists[a]["common"] for a in artists),
"more_other":sum(artists[a]["other"] for a in artists if artists[a]["common"] != 0),
"unique_other":sum(artists[a]["other"] for a in artists if artists[a]["common"] == 0)
}
total = sum(result[c] for c in result)
percentages = {c:result[c]*100/total for c in result}
css = []
cumulative = 0
for color,category in [
("rgba(255,255,255,0.2)","unique_self"),
("rgba(255,255,255,0.5)","more_self"),
("white","common"),
("rgba(255,255,255,0.5)","more_other"),
("rgba(255,255,255,0.2)","unique_other")]:
cumulative += percentages[category]
css.append(color + " " + str(cumulative) + "%")
fullmatch = percentages["common"]
partialmatch = percentages["more_self"] + percentages["more_other"]
match = fullmatch + (partialmatch)/2
pixel_fullmatch = fullmatch * 2.5
pixel_partialmatch = (fullmatch+partialmatch) * 2.5
match = min(match,100)
matchcolor = format(int(min(1,match/50)*255),"02x") * 2 + format(int(max(0,match/50-1)*255),"02x")
return {
"KEY_CIRCLE_CSS":",".join(css),
"KEY_CICLE_COLOR":matchcolor,
"KEY_MATCH":str(round(match,2)),
"KEY_FULLMATCH":str(int(pixel_fullmatch)),
"KEY_PARTIALMATCH":str(int(pixel_partialmatch)),
"KEY_NAME_SELF":owninfo["name"],
"KEY_NAME_OTHER":strangerinfo["name"],
"KEY_BESTARTIST_LINK":artistLink(best[0]),
"KEY_BESTARTIST_IMAGE":getArtistImage(best[0])
},[]

View File

@ -7,7 +7,7 @@
<script src="javascript/cookies.js"></script> <script src="javascript/cookies.js"></script>
</head> </head>
<body onload="insertAPIKeyFromCookie()"> <body>
<table class="top_info"> <table class="top_info">
<tr> <tr>
<td class="image"> <td class="image">

View File

@ -1,32 +1,81 @@
apikeycorrect = false; apikeycorrect = false;
function insertAPIKeyFromCookie() { var cookies = {};
cookies = decodeURIComponent(document.cookie).split(';');
for(var i = 0; i <cookies.length; i++) { function getCookies() {
cookies[i] = cookies[i].trim() cookiestrings = decodeURIComponent(document.cookie).split(';');
if (cookies[i].startsWith("apikey=")) { for(var i = 0; i <cookiestrings.length; i++) {
document.getElementById("apikey").value = cookies[i].replace("apikey=","") cookiestrings[i] = cookiestrings[i].trim();
checkAPIkey() [key,value] = cookiestrings[i].split("=");
} cookies[key] = value;
}
}
// always on document load, but call specifically when needed early
document.addEventListener("load",getCookies);
function setCookie(key,val,session=true) {
cookies[key] = val;
if (!session) {
var d = new Date();
d.setTime(d.getTime() + (500*24*60*60*1000));
expirestr = "expires=" + d.toUTCString();
}
else {
expirestr = ""
}
document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(val) + ";" + expirestr;
}
function saveCookies() {
for (var c in cookies) {
document.cookie = encodeURIComponent(c) + "=" + encodeURIComponent(cookies[c]);
} }
} }
/// RANGE SELECTORS
// in rangeselect.js
/// API KEY
function insertAPIKeyFromCookie() {
element = document.getElementById("apikey")
if (element != null && element != undefined) {
getCookies();
key = cookies["apikey"];
if (key != null && key != undefined) {
element.value = key;
checkAPIkey();
}
}
}
window.addEventListener("load",insertAPIKeyFromCookie);
function saveAPIkey() { function saveAPIkey() {
key = document.getElementById("apikey").value key = APIkey();
document.cookie = "apikey=" + encodeURIComponent(key) setCookie("apikey",key,false);
} }
function checkAPIkey() { function checkAPIkey() {
saveAPIkey()
url = "/api/test?key=" + document.getElementById("apikey").value url = "/api/test?key=" + APIkey()
var xhttp = new XMLHttpRequest(); var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 204 || this.status == 205)) { if (this.readyState == 4 && (this.status == 204 || this.status == 205)) {
document.getElementById("apikey").style.backgroundColor = "lawngreen" document.getElementById("apikey").style.backgroundColor = "lawngreen"
apikeycorrect = true apikeycorrect = true
saveAPIkey();
} }
else { else {
document.getElementById("apikey").style.backgroundColor = "red" document.getElementById("apikey").style.backgroundColor = "red"

View File

@ -1,28 +1,53 @@
function showRange(identifier,unit) { function showRange(identifier,unit) {
// Make all modules disappear // Make all modules disappear
modules = document.getElementsByClassName("stat_module_" + identifier) modules = document.getElementsByClassName("stat_module_" + identifier);
for (var i=0;i<modules.length;i++) { for (var i=0;i<modules.length;i++) {
//modules[i].setAttribute("style","width:0px;overflow:hidden;") //modules[i].setAttribute("style","width:0px;overflow:hidden;")
// cheesy trick to make the allocated space always whatever the biggest module needs // cheesy trick to make the allocated space always whatever the biggest module needs
// somehow that messes up pulse on the start page tho // somehow that messes up pulse on the start page tho
modules[i].setAttribute("style","display:none;") modules[i].setAttribute("style","display:none;");
} }
// Make requested module appear // Make requested module appear
reactivate = document.getElementsByClassName(identifier + "_" + unit) reactivate = document.getElementsByClassName(identifier + "_" + unit);
for (var i=0;i<reactivate.length;i++) { for (var i=0;i<reactivate.length;i++) {
reactivate[i].setAttribute("style","") reactivate[i].setAttribute("style","");
} }
// Set all selectors to unselected // Set all selectors to unselected
selectors = document.getElementsByClassName("stat_selector_" + identifier) selectors = document.getElementsByClassName("stat_selector_" + identifier);
for (var i=0;i<selectors.length;i++) { for (var i=0;i<selectors.length;i++) {
selectors[i].setAttribute("style","") selectors[i].setAttribute("style","");
} }
// Set the active selector to selected // Set the active selector to selected
reactivate = document.getElementsByClassName("selector_" + identifier + "_" + unit) reactivate = document.getElementsByClassName("selector_" + identifier + "_" + unit);
for (var i=0;i<reactivate.length;i++) { for (var i=0;i<reactivate.length;i++) {
reactivate[i].setAttribute("style","opacity:0.5;") reactivate[i].setAttribute("style","opacity:0.5;");
} }
links = document.getElementsByClassName("stat_link_" + identifier);
for (let l of links) {
var a = l.href.split("=");
a.splice(-1);
a.push(unit);
l.href = a.join("=");
}
} }
function showRangeManual(identifier,unit) {
showRange(identifier,unit);
setCookie("rangeselect_" + identifier,unit);
}
document.addEventListener('DOMContentLoaded',function() {
getCookies();
for (c in cookies) {
if (c.startsWith("rangeselect_")) {
showRange(c.slice(12),cookies[c]);
}
}
})

134
website/less/grisons.less Normal file
View File

@ -0,0 +1,134 @@
/**
COMMON STYLES FOR MALOJA, ALBULA AND POSSIBLY OTHERS
**/
@BASE_COLOR: #333337;
@BASE_COLOR_DARK: #0a0a0a;
@BASE_COLOR_LIGHT: #444447;
@TEXT_COLOR: beige;
@TEXT_COLOR_SELECTED: fadeout(@TEXT_COLOR,40%);
@TEXT_COLOR_SECONDARY: #bbb;
@TEXT_COLOR_TERTIARY: grey;
@FOCUS_COLOR: yellow;
@CONTROL_ELEMENT_BG_COLOR: rgba(0,255,255,0.1);
@CONTROL_ELEMENT_FG_COLOR: rgba(103,85,0,0.7);
@CONTROL_ELEMENT_FOCUS_COLOR: gold;
@BUTTON_BG_COLOR: @TEXT_COLOR;
@BUTTON_FOCUS_BG_COLOR: @FOCUS_COLOR;
@BUTTON_FG_COLOR: @BASE_COLOR;
@BUTTON_FOCUS_FG_COLOR: @BASE_COLOR;
//@import url('https://fonts.googleapis.com/css?family=Ubuntu');
body {
background-color: @BASE_COLOR;
color: @TEXT_COLOR;
font-family:"Ubuntu";
}
/* TOP INFO TABLE */
table.top_info td.image div {
margin-right:20px;
margin-bottom:20px;
background-size:cover;
background-position:center;
height:174px;
width:174px
}
table.top_info td.text {
vertical-align: top;
}
table.top_info td.text h1 {
display:inline;
padding-right:5px;
}
table.top_info td.text table.image_row td {
height:50px;
width:50px;
background-size:cover;
background-position:center;
background-repeat: no-repeat;
opacity:0.5;
filter: grayscale(80%);
}
table.top_info td.text table.image_row td:hover {
opacity:1;
filter: grayscale(0%);
}
/** SCROLLBAR **/
::-webkit-scrollbar {
width: 8px;
cursor: pointer;
}
::-webkit-scrollbar-track {
background: grey;
background-color: @CONTROL_ELEMENT_BG_COLOR;
}
::-webkit-scrollbar-thumb {
background-color: @CONTROL_ELEMENT_FG_COLOR;
}
::-webkit-scrollbar-thumb:hover {
background: @CONTROL_ELEMENT_FOCUS_COLOR;
}
[onclick]:hover, a:hover {
cursor: pointer;
}
/** HOVERABLE LOAD/PROGRESS BAR **/
div.grisons_bar {
background-color: @CONTROL_ELEMENT_BG_COLOR;
}
div.grisons_bar>div {
height:100%;
background-color: @CONTROL_ELEMENT_FG_COLOR;
}
div.grisons_bar:hover>div {
background-color: @CONTROL_ELEMENT_FOCUS_COLOR;
}
/** LINKS **/
a {
color:inherit;
text-decoration:none;
}
// for links in running text
a.textlink {
color:@FOCUS_COLOR;
}
a.hidelink:hover {
text-decoration:none;
}
a:hover {
text-decoration:underline;
}

View File

@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcg72j00.woff2) format('woff2');
}
/* cyrillic */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcw72j00.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKfA72j00.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* latin-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
}
/* latin */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKfw72.woff2) format('woff2');
}

View File

@ -1,9 +1,6 @@
@import url('https://fonts.googleapis.com/css?family=Ubuntu'); @import "website/less/grisons";
body { body {
background-color:#333337;
color:beige;
font-family:"Ubuntu";
padding:15px; padding:15px;
padding-bottom:35px; padding-bottom:35px;
/** /**
@ -15,21 +12,6 @@ body {
*/ */
} }
a {
color:inherit;
text-decoration:none;
}
a.textlink {
color:gold;
}
a.hidelink:hover {
text-decoration:none;
}
a:hover {
text-decoration:underline;
}
input[type="date"] { input[type="date"] {
@ -42,6 +24,9 @@ input[type="date"] {
} }
/** /**
Header (unused) Header (unused)
**/ **/
@ -76,7 +61,7 @@ div.footer {
position:fixed; position:fixed;
height:20px; height:20px;
/**width:100%;**/ /**width:100%;**/
background-color:rgba(10,10,10,0.9); background-color:@BASE_COLOR_DARK;
bottom:0px; bottom:0px;
left:0px; left:0px;
right:0px; right:0px;
@ -185,7 +170,7 @@ div.searchresults table.searchresults_tracks td span:nth-child(1) {
position:fixed; position:fixed;
/*height:30px;*/ /*height:30px;*/
/**width:100%;**/ /**width:100%;**/
background-color:rgba(10,10,10,0.9); background-color:@BASE_COLOR_DARK;
bottom:0px; bottom:0px;
left:0px; left:0px;
right:0px; right:0px;
@ -218,36 +203,6 @@ div.searchresults table.searchresults_tracks td span:nth-child(1) {
/*
**
**
** TOP INFO TABLE
**
**
*/
table.top_info td.image {
padding:20px;
padding-left:0px;
padding-top:0px;
}
table.top_info td.image div {
background-size:cover;
background-position:center;
height:174px;
width:174px
}
table.top_info td.text {
vertical-align: top;
padding-left: 30px;
}
table.top_info td.text h1 {
display:inline;
padding-right:5px;
}
p.desc a { p.desc a {
padding-left:20px; padding-left:20px;
@ -257,7 +212,10 @@ p.desc a {
background-image:url("https://www.last.fm/static/images/lastfm_avatar_twitter.66cd2c48ce03.png"); background-image:url("https://www.last.fm/static/images/lastfm_avatar_twitter.66cd2c48ce03.png");
} }
table.top_info + .stat_module_topartists table,
table.top_info + .stat_module_toptracks table {
margin:15px 0;
}
/* /*
** **
@ -268,17 +226,22 @@ p.desc a {
*/ */
.paginate {
text-align: center;
padding:30px;
}
.stats { .stats {
color:grey; color:@TEXT_COLOR_TERTIARY;
} }
.rank { .rank {
text-align:right; text-align:right;
color:grey; color:@TEXT_COLOR_TERTIARY;
} }
.extra { .extra {
color:gray; /*sue me*/ color:@TEXT_COLOR_TERTIARY;
font-size:80%; font-size:80%;
} }
@ -292,14 +255,14 @@ input#apikey {
input.simpleinput { input.simpleinput {
font-family:'Ubuntu'; font-family:'Ubuntu';
color:beige; color:@TEXT_COLOR;
outline:none; outline:none;
border-top: 0px solid; border-top: 0px solid;
border-left: 0px solid; border-left: 0px solid;
border-right: 0px solid; border-right: 0px solid;
padding:2px; padding:2px;
background-color:inherit; background-color:inherit;
border-bottom: 1px solid beige; border-bottom: 1px solid @TEXT_COLOR;
} }
@ -379,6 +342,15 @@ img.certrecord {
height:30px; height:30px;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
img.certrecord_small {
height:20px;
vertical-align: text-bottom;
}
img.star {
height:20px;
vertical-align: text-bottom;
}
/* /*
@ -430,7 +402,7 @@ table.list tr:hover {
table.list td.time { table.list td.time {
width:11%; width:11%;
color:gray; color:@TEXT_COLOR_TERTIARY;
} }
@ -476,10 +448,13 @@ table.list td.artists,td.artist,td.title,td.track {
} }
table.list td.track span.artist_in_trackcolumn { table.list td.track span.artist_in_trackcolumn {
color:#bbb; color:@TEXT_COLOR_SECONDARY;
} }
table.list td.track a.trackProviderSearch {
margin-right: 5px;
padding: 0 10px;
}
@ -519,23 +494,23 @@ table.list td.amount {
} }
table.list td.bar { table.list td.bar {
width:500px; width:500px;
background-color:#333337; background-color:@BASE_COLOR;
/* Remove 5er separators for bars */ /* Remove 5er separators for bars */
/*border-color:rgba(0,0,0,0)!important;*/ /*border-color:rgba(0,0,0,0)!important;*/
} }
table.list td.bar div { table.list td.bar div {
background-color:beige; background-color:@TEXT_COLOR;
height:20px; /* can only do this absolute apparently */ height:20px; /* can only do this absolute apparently */
position:relative; position:relative;
} }
table.list tr:hover td.bar div { table.list tr:hover td.bar div {
background-color:yellow; background-color:@FOCUS_COLOR;
cursor:pointer; cursor:pointer;
} }
table.list td.chart { table.list td.chart {
width:500px; width:500px;
background-color:#333337; background-color:@BASE_COLOR;
/* Remove 5er separators for bars */ /* Remove 5er separators for bars */
/*border-color:rgba(0,0,0,0)!important;*/ /*border-color:rgba(0,0,0,0)!important;*/
} }
@ -581,8 +556,14 @@ table.list tr td.button {
table.list td.button div { table.list td.button div {
background-color:yellow; background-color:@BUTTON_BG_COLOR;
color:#333337; color:@BUTTON_FG_COLOR;
padding:3px;
border-radius:4px;
}
table.list td.button div:hover {
background-color:@BUTTON_FOCUS_BG_COLOR;
color:@BUTTON_FOCUS_FG_COLOR;
padding:3px; padding:3px;
border-radius:4px; border-radius:4px;
} }

View File

@ -154,7 +154,7 @@
<body onload="insertAPIKeyFromCookie()"> <body>
<table class="top_info"> <table class="top_info">
<tr> <tr>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
website/media/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
website/media/star_alt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -9,10 +9,10 @@ def instructions(keys):
from htmlmodules import module_performance, module_filterselection from htmlmodules import module_performance, module_filterselection
from malojatime import range_desc, delimit_desc from malojatime import range_desc, delimit_desc
filterkeys, timekeys, delimitkeys, _ = uri_to_internal(keys) filterkeys, timekeys, delimitkeys, paginatekeys = uri_to_internal(keys)
#equivalent pulse chart #equivalent pulse chart
pulselink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys}) pulselink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys,**paginatekeys})
pulselink = "/pulse?" + compose_querystring(pulselink_keys) pulselink = "/pulse?" + compose_querystring(pulselink_keys)
pulselink = "<a href=\"" + pulselink + "\"><span>View Pulse</span></a>" pulselink = "<a href=\"" + pulselink + "\"><span>View Pulse</span></a>"
@ -54,7 +54,7 @@ def instructions(keys):
html_performance = module_performance(**filterkeys,**timekeys,**delimitkeys) html_performance = module_performance(**filterkeys,**timekeys,**delimitkeys,**paginatekeys)
replace = { replace = {
"KEY_PULSE_LINK":pulselink, "KEY_PULSE_LINK":pulselink,

46
website/proxy.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Maloja - Proxyscrobble</title>
<script src="javascript/cookies.js"></script>
<script>
window.addEventListener("load",function(){
try {
document.getElementById("lastfmlink").href += window.location.href;
}
catch (e) {
}
});
</script>
</head>
<body>
<table class="top_info">
<tr>
<td class="image">
<div style="background-image:url('/favicon.png')"></div>
</td>
<td class="text">
<h1>Proxyscrobble</h1>
<p class="desc">Duplicate your scrobbles to another service.
Your API key is required to make any changes to the server: <input id='apikey' onchange='checkAPIkey()' style='width:300px;'/></p>
</td>
</tr>
</table>
<table class="list">
<tr>
<td>Last.fm</td>
KEY_STATUS_LASTFM
</tr>
</table>
</body>
</html>

53
website/proxy.py Normal file
View File

@ -0,0 +1,53 @@
from doreah.settings import get_settings, update_settings
import urllib.request
import hashlib
import xml.etree.ElementTree as ET
from bottle import redirect, request
from database import checkAPIkey
from external import lfmbuild
def instructions(keys):
authenticated = False
if "Cookie" in request.headers:
cookies = request.headers["Cookie"].split(";")
for c in cookies:
if c.strip().startswith("apikey="):
authenticated = checkAPIkey(c.strip()[7:])
if "token" in keys and authenticated:
token = keys.get("token")
parameters = {
"method":"auth.getSession",
"token":token,
"api_key":get_settings("LASTFM_API_KEY")
}
response = urllib.request.urlopen("http://ws.audioscrobbler.com/2.0/?" + lfmbuild(parameters))
xml = response.read()
data = ET.fromstring(xml)
if data.attrib.get("status") == "ok":
username = data.find("session").find("name").text
sessionkey = data.find("session").find("key").text
update_settings("settings/settings.ini",{"LASTFM_API_SK":sessionkey,"LASTFM_USERNAME":username},create_new=True)
return "/proxy"
else:
key,secret,sessionkey,name = get_settings("LASTFM_API_KEY","LASTFM_API_SECRET","LASTFM_API_SK","LASTFM_USERNAME")
if key is None:
lastfm = "<td>No Last.fm key provided</td>"
elif secret is None:
lastfm = "<td>No Last.fm secret provided</td>"
elif sessionkey is None and authenticated:
url = "http://www.last.fm/api/auth/?api_key=" + key + "&cb="
lastfm = "<td class='button'><a id='lastfmlink' href='" + url + "'><div>Connect</div></a></td>"
elif sessionkey is None:
lastfm = "<td>Not active</td>"
else:
lastfm = "<td>Account: " + name + "</td>"
return {"KEY_STATUS_LASTFM":lastfm},[]

View File

@ -9,11 +9,11 @@ def instructions(keys):
from htmlmodules import module_pulse, module_filterselection from htmlmodules import module_pulse, module_filterselection
from malojatime import range_desc, delimit_desc from malojatime import range_desc, delimit_desc
filterkeys, timekeys, delimitkeys, _ = uri_to_internal(keys) filterkeys, timekeys, delimitkeys, paginatekeys = uri_to_internal(keys)
#equivalent performance chart if we're not looking at the overall pulse #equivalent performance chart if we're not looking at the overall pulse
if len(filterkeys) != 0: if len(filterkeys) != 0:
performancelink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys}) performancelink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys,**paginatekeys})
performancelink = "/performance?" + compose_querystring(performancelink_keys) performancelink = "/performance?" + compose_querystring(performancelink_keys)
performancelink = "<a href=\"" + performancelink + "\"><span>View Rankings</span></a>" performancelink = "<a href=\"" + performancelink + "\"><span>View Rankings</span></a>"
@ -57,7 +57,7 @@ def instructions(keys):
html_pulse = module_pulse(**filterkeys,**timekeys,**delimitkeys) html_pulse = module_pulse(**filterkeys,**timekeys,**delimitkeys,**paginatekeys)
replace = { replace = {
"KEY_RANKINGS_LINK":performancelink, "KEY_RANKINGS_LINK":performancelink,

View File

@ -58,7 +58,7 @@
</head> </head>
<body onload="replace();insertAPIKeyFromCookie()"> <body onload="replace()">
<table class="top_info"> <table class="top_info">
<tr> <tr>
<td class="image"> <td class="image">

View File

@ -4,10 +4,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Maloja</title> <title>Maloja</title>
<script src="javascript/rangeselect.js"></script>
<script>document.addEventListener('DOMContentLoaded',function() { <script>document.addEventListener('DOMContentLoaded',function() {
KEY_JS_INIT_RANGES KEY_JS_INIT_RANGES
})</script> })</script>
<script src="javascript/cookies.js"></script>
<script src="javascript/rangeselect.js"></script>
</head> </head>
@ -18,15 +20,15 @@
</div>--> </div>-->
<h1><a href="/charts_artists?max=50">Top Artists</a></h1> <h1><a class="stat_link_topartists" href="/charts_artists?in=alltime">Top Artists</a></h1>
<!--All Time | This Year | This Month | This Week--> <!--All Time | This Year | This Month | This Week-->
<span onclick="showRange('topartists','week')" class="stat_selector_topartists selector_topartists_week">This Week</span> <span onclick="showRangeManual('topartists','week')" class="stat_selector_topartists selector_topartists_week">This Week</span>
| <span onclick="showRange('topartists','month')" class="stat_selector_topartists selector_topartists_month">This Month</span> | <span onclick="showRangeManual('topartists','month')" class="stat_selector_topartists selector_topartists_month">This Month</span>
| <span onclick="showRange('topartists','year')" class="stat_selector_topartists selector_topartists_year">This Year</span> | <span onclick="showRangeManual('topartists','year')" class="stat_selector_topartists selector_topartists_year">This Year</span>
| <span onclick="showRange('topartists','alltime')" class="stat_selector_topartists selector_topartists_alltime" style="opacity:0.5;">All Time</span> | <span onclick="showRangeManual('topartists','alltime')" class="stat_selector_topartists selector_topartists_alltime" style="opacity:0.5;">All Time</span>
<br/><br/> <br/><br/>
@ -38,13 +40,13 @@
<h1><a href="/charts_tracks?max=50">Top Tracks</a></h1> <h1><a class="stat_link_toptracks" href="/charts_tracks?in=alltime">Top Tracks</a></h1>
<span onclick="showRange('toptracks','week')" class="stat_selector_toptracks selector_toptracks_week">This Week</span> <span onclick="showRange('toptracks','week')" class="stat_selector_toptracks selector_toptracks_week">This Week</span>
| <span onclick="showRange('toptracks','month')" class="stat_selector_toptracks selector_toptracks_month">This Month</span> | <span onclick="showRangeManual('toptracks','month')" class="stat_selector_toptracks selector_toptracks_month">This Month</span>
| <span onclick="showRange('toptracks','year')" class="stat_selector_toptracks selector_toptracks_year">This Year</span> | <span onclick="showRangeManual('toptracks','year')" class="stat_selector_toptracks selector_toptracks_year">This Year</span>
| <span onclick="showRange('toptracks','alltime')" class="stat_selector_toptracks selector_toptracks_alltime" style="opacity:0.5;">All Time</span> | <span onclick="showRangeManual('toptracks','alltime')" class="stat_selector_toptracks selector_toptracks_alltime" style="opacity:0.5;">All Time</span>
<br/><br/> <br/><br/>
@ -59,7 +61,7 @@
<div class="sidelist"> <div class="sidelist">
<h1><a href="/scrobbles?max=100">Last Scrobbles</a></h1> <h1><a href="/scrobbles">Last Scrobbles</a></h1>
<span class="stats">Today</span> KEY_SCROBBLE_NUM_TODAY <span class="stats">Today</span> KEY_SCROBBLE_NUM_TODAY
<span class="stats">This Week</span> KEY_SCROBBLE_NUM_WEEK <span class="stats">This Week</span> KEY_SCROBBLE_NUM_WEEK
<span class="stats">This Month</span> KEY_SCROBBLE_NUM_MONTH <span class="stats">This Month</span> KEY_SCROBBLE_NUM_MONTH
@ -73,7 +75,7 @@
<br/> <br/>
<h1><a href="/pulse?step=month&trail=1">Pulse</a></h1> <h1><a class="stat_link_pulse" href="/pulse?trail=1&step=month">Pulse</a></h1>
<!-- <!--
<a href="/pulse?step=day&trail=1">Days</a> <a href="/pulse?step=day&trail=1">Days</a>
<a href="/pulse?step=week&trail=1">Weeks</a> <a href="/pulse?step=week&trail=1">Weeks</a>
@ -81,10 +83,10 @@
<a href="/pulse?step=year&trail=1">Years</a> <a href="/pulse?step=year&trail=1">Years</a>
--> -->
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span> <span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span> | <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span> | <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span> | <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<!-- <!--
### this is for extra views of the current canonical week / month / year ### this is for extra views of the current canonical week / month / year
<br/> <br/>
@ -94,10 +96,10 @@
--> -->
<br/><br/> <br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span> <span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span> <span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span> <span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
<!-- <!--
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEK</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEK</span>
<span class="stat_module_pulse pulse_month" style="display:none;">KEY_PULSE_MONTH</span> <span class="stat_module_pulse pulse_month" style="display:none;">KEY_PULSE_MONTH</span>

View File

@ -12,7 +12,7 @@ def instructions(keys):
# commands to execute on load for default ranges # commands to execute on load for default ranges
js_command = "showRange('topartists','" + get_settings("DEFAULT_RANGE_CHARTS_ARTISTS") + "');" js_command = "showRange('topartists','" + get_settings("DEFAULT_RANGE_CHARTS_ARTISTS") + "');"
js_command += "showRange('toptracks','" + get_settings("DEFAULT_RANGE_CHARTS_TRACKS") + "');" js_command += "showRange('toptracks','" + get_settings("DEFAULT_RANGE_CHARTS_TRACKS") + "');"
js_command += "showRange('pulse','" + get_settings("DEFAULT_RANGE_PULSE") + "');" js_command += "showRange('pulse','" + get_settings("DEFAULT_STEP_PULSE") + "');"
clock() clock()

View File

@ -4,7 +4,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Maloja - KEY_TRACKTITLE</title> <title>Maloja - KEY_TRACKTITLE</title>
<script src="javascript/rangeselect.js" async></script> <script src="javascript/cookies.js" ></script>
<script src="javascript/rangeselect.js"></script>
</head> </head>
<body> <body>
@ -15,12 +16,12 @@
</td> </td>
<td class="text"> <td class="text">
<span>KEY_ARTISTS</span><br/> <span>KEY_ARTISTS</span><br/>
<h1>KEY_TRACKTITLE</h1> KEY_CERTS <span class="rank"><a href="/charts_tracks?max=100">KEY_POSITION</a></span> <h1>KEY_TRACKTITLE</h1> KEY_CERTS <span class="rank"><a href="/charts_tracks">KEY_POSITION</a></span>
<p class="stats"><a href="/scrobbles?KEY_SCROBBLELINK">KEY_SCROBBLES Scrobbles</a></p> <p class="stats"><a href="/scrobbles?KEY_SCROBBLELINK">KEY_SCROBBLES Scrobbles</a></p>
<p class="desc"></p> <p class="desc"></p>
<span>KEY_MEDALS</span> <span>KEY_MEDALS</span> <span>KEY_TOPWEEKS</span>
</td> </td>
</tr> </tr>
</table> </table>
@ -29,32 +30,32 @@
<table class="twopart"> <table class="twopart">
<tr> <tr>
<td> <td>
<h2><a href='/pulse?KEY_SCROBBLELINK&step=year&trail=1'>Pulse</a></h2> <h2><a class="stat_link_pulse" href='/pulse?KEY_SCROBBLELINK&trail=1&step=month'>Pulse</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span> <span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span> | <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span> | <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span> | <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/> <br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span> <span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span> <span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span> <span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
</td> </td>
<td> <td>
<h2><a href='/performance?KEY_SCROBBLELINK&step=year&trail=1'>Performance</a></h2> <h2><a class="stat_link_pulse" href='/performance?KEY_SCROBBLELINK&trail=1&step=month'>Performance</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span> <span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span> | <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span> | <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span> | <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/> <br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PERFORMANCE_MONTHS</span> <span class="stat_module_pulse pulse_month">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PERFORMANCE_DAYS</span> <span class="stat_module_pulse pulse_day" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PERFORMANCE_YEARS</span> <span class="stat_module_pulse pulse_year" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PERFORMANCE_WEEKS</span> <span class="stat_module_pulse pulse_week" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -29,13 +29,19 @@ def instructions(keys):
if "medals" in data and data["medals"] is not None: if "medals" in data and data["medals"] is not None:
if "gold" in data["medals"]: if "gold" in data["medals"]:
for y in data["medals"]["gold"]: for y in data["medals"]["gold"]:
html_medals += "<a title='Best Track in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Best Track in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "silver" in data["medals"]: if "silver" in data["medals"]:
for y in data["medals"]["silver"]: for y in data["medals"]["silver"]:
html_medals += "<a title='Second Best Track in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Second Best Track in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "bronze" in data["medals"]: if "bronze" in data["medals"]:
for y in data["medals"]["bronze"]: for y in data["medals"]["bronze"]:
html_medals += "<a title='Third Best Track in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>" html_medals += "<a title='Third Best Track in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_topweeks = ""
if data.get("topweeks") not in [0,None]:
link = "/performance?" + compose_querystring(keys) + "&trail=1&step=week"
title = str(data["topweeks"]) + " weeks on #1"
html_topweeks = "<a title='" + title + "' href='" + link + "'><img class='star' src='/media/star.png' />" + str(data["topweeks"]) + "</a>"
@ -65,6 +71,7 @@ def instructions(keys):
"KEY_SCROBBLELINK":compose_querystring(keys), "KEY_SCROBBLELINK":compose_querystring(keys),
"KEY_MEDALS":html_medals, "KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert, "KEY_CERTS":html_cert,
"KEY_TOPWEEKS":html_topweeks,
"KEY_SCROBBLELIST":html_scrobbles, "KEY_SCROBBLELIST":html_scrobbles,
# pulse # pulse
"KEY_PULSE_MONTHS":html_pulse_months, "KEY_PULSE_MONTHS":html_pulse_months,