1
0
mirror of https://github.com/krateng/maloja.git synced 2023-08-10 21:12:55 +03:00

Merged master changes

This commit is contained in:
Krateng
2019-06-13 12:22:02 +02:00
23 changed files with 201 additions and 182 deletions

2
.gitignore vendored
View File

@@ -1,7 +1,7 @@
# generic temporary / dev files
*.pyc
*.sh
*.txt
*.note
*.xcf
nohup.out
/.dev

View File

@@ -19,7 +19,9 @@ Also neat: You can use your **custom artist or track images**.
* [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython)
* [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle)
* [waitress](https://docs.pylonsproject.org/projects/waitress/) - [GitHub](https://github.com/Pylons/waitress)
* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.7.2)
* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.9.1)
* [nimrodel](https://pypi.org/project/nimrodel/) - [GitHub](https://github.com/krateng/nimrodel) (at least Version 0.4.9)
* [setproctitle](https://pypi.org/project/setproctitle/) - [GitHub](https://github.com/dvarrazzo/py-setproctitle)
* If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Fanart.tv](https://fanart.tv/get-an-api-key/). These are free of charge!
## How to install
@@ -28,13 +30,15 @@ Also neat: You can use your **custom artist or track images**.
./maloja install
2) Start the server with
2) Install required packages with
pip3 install -r requirements.txt
3) Start the server with
maloja start
If you're missing packages, the console output will tell you so. Install them.
2) (Recommended) Put your server behind a reverse proxy for SSL encryption.
4) (Recommended) Put your server behind a reverse proxy for SSL encryption.
## How to use
@@ -58,13 +62,15 @@ If you didn't install Maloja from the package (and therefore don't have it in `/
3) Various folders have `.info` files with more information on how to use their associated features.
4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
## How to scrobble
### Native API
If you use Plex Web or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings.
If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key`.
If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as from-data or json.
### Standard-compliant API

View File

@@ -43,17 +43,12 @@ def handler(apiname,version):
return cls
return deco
def handle(path,keys,headers,auth):
def handle(path,keys):
print("API request: " + str(path))
print("Keys:")
for k in keys:
print("\t" + str(k) + ": " + str(keys.get(k)))
print("Headers:")
for h in headers:
print("\t" + str(h) + ": " + str(headers.get(h)))
print("Auth: " + str(auth))
print("\t",k,":",keys.get(k))
keys = {**keys,**headers}
if len(path)>1 and (path[0],path[1]) in handlers:
handler = handlers[(path[0],path[1])]
@@ -179,7 +174,7 @@ class LBrnz1(APIHandler):
}
self.errors = {
BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}),
InvalidAuthException:(401,{"code":401,"error":"Bad Auth"}),
InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}),
InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}),
MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}),
ScrobblingException:(500,{"code":500,"error":"Unspecified server error."})
@@ -191,7 +186,7 @@ class LBrnz1(APIHandler):
def submit(self,pathnodes,keys):
try:
token = keys.get("Authorization").replace("token ","").strip()
token = keys.get("Authorization").replace("token ","").replace("Token ","").strip()
except:
raise BadAuthException()

View File

@@ -1,19 +1,23 @@
# server
from bottle import request, response, FormsDict
# rest of the project
from cleanup import *
from utilities import *
from malojatime import *
from cleanup import CleanerAgent, CollectorAgent
import utilities
from malojatime import register_scrobbletime, time_stamps, ranges
from urihandler import uri_to_internal, internal_to_uri, compose_querystring
import compliant_api
# doreah toolkit
from doreah.logging import log
from doreah import tsv
from doreah import settings
from doreah.caching import Cache, DeepCache
try:
from doreah.persistence import DiskDict
except: pass
import doreah
# nimrodel API
from nimrodel import EAPI as API
from nimrodel import Multi
# technical
import os
import datetime
@@ -27,7 +31,6 @@ import urllib
dblock = Lock() #global database lock
SCROBBLES = [] # Format: tuple(track_ref,timestamp,saved)
@@ -182,54 +185,13 @@ def getTrackID(artists,title):
########
# silly patch to get old syntax working without dbserver
# function to register all the functions to the real server
def register_subroutes(server,path):
for subpath in dbserver.handlers_get:
func = dbserver.handlers_get[subpath]
decorator = server.get(path + subpath)
decorator(func)
for subpath in dbserver.handlers_post:
func = dbserver.handlers_post[subpath]
decorator = server.post(path + subpath)
decorator(func)
dbserver = API(delay=True,path="api")
# fake server
class FakeBottle:
def __init__(self):
self.handlers_get = {}
self.handlers_post = {}
# these functions pretend that they're the bottle decorators, but only write
# down which functions asked for them so they can later report their names
# to the real bottle server
def get(self,path):
def register(func):
self.handlers_get[path] = func
return func
return register
def post(self,path):
def register(func):
self.handlers_post[path] = func
return func
return register
def route(self,path):
return self.get(path)
dbserver = FakeBottle()
@dbserver.route("/test")
def test_server():
apikey = request.query.get("key")
@dbserver.get("test")
def test_server(key=None):
response.set_header("Access-Control-Allow-Origin","*")
if apikey is not None and not (checkAPIkey(apikey)):
if key is not None and not (checkAPIkey(key)):
response.status = 403
return "Wrong API key"
@@ -247,9 +209,8 @@ def test_server():
## All database functions are separated - the external wrapper only reads the request keys, converts them into lists and renames them where necessary, and puts the end result in a dict if not already so it can be returned as json
@dbserver.route("/scrobbles")
def get_scrobbles_external():
keys = FormsDict.decode(request.query)
@dbserver.get("scrobbles")
def get_scrobbles_external(**keys):
k_filter, k_time, _, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_amount}
@@ -277,9 +238,8 @@ def get_scrobbles(**keys):
# return {"scrobbles":len(SCROBBLES),"tracks":len(TRACKS),"artists":len(ARTISTS)}
@dbserver.route("/numscrobbles")
def get_scrobbles_num_external():
keys = FormsDict.decode(request.query)
@dbserver.get("numscrobbles")
def get_scrobbles_num_external(**keys):
k_filter, k_time, _, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_amount}
@@ -342,9 +302,8 @@ def get_scrobbles_num(**keys):
@dbserver.route("/tracks")
def get_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("tracks")
def get_tracks_external(**keys):
k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter}
@@ -366,7 +325,7 @@ def get_tracks(artist=None):
#ls = [t for t in tracklist if (artist in t["artists"]) or (artist==None)]
@dbserver.route("/artists")
@dbserver.get("artists")
def get_artists_external():
result = get_artists()
return {"list":result}
@@ -380,9 +339,8 @@ def get_artists():
@dbserver.route("/charts/artists")
def get_charts_artists_external():
keys = FormsDict.decode(request.query)
@dbserver.get("charts/artists")
def get_charts_artists_external(**keys):
_, k_time, _, _ = uri_to_internal(keys)
ckeys = {**k_time}
@@ -397,9 +355,8 @@ def get_charts_artists(**keys):
@dbserver.route("/charts/tracks")
def get_charts_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("charts/tracks")
def get_charts_tracks_external(**keys):
k_filter, k_time, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter, **k_time}
@@ -417,9 +374,8 @@ def get_charts_tracks(**keys):
@dbserver.route("/pulse")
def get_pulse_external():
keys = FormsDict.decode(request.query)
@dbserver.get("pulse")
def get_pulse_external(**keys):
k_filter, k_time, k_internal, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
@@ -440,9 +396,8 @@ def get_pulse(**keys):
@dbserver.route("/performance")
def get_performance_external():
keys = FormsDict.decode(request.query)
@dbserver.get("performance")
def get_performance_external(**keys):
k_filter, k_time, k_internal, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
@@ -480,10 +435,8 @@ def get_performance(**keys):
@dbserver.route("/top/artists")
def get_top_artists_external():
keys = FormsDict.decode(request.query)
@dbserver.get("top/artists")
def get_top_artists_external(**keys):
_, k_time, k_internal, _ = uri_to_internal(keys)
ckeys = {**k_time, **k_internal}
@@ -513,9 +466,8 @@ def get_top_artists(**keys):
@dbserver.route("/top/tracks")
def get_top_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("top/tracks")
def get_top_tracks_external(**keys):
_, k_time, k_internal, _ = uri_to_internal(keys)
ckeys = {**k_time, **k_internal}
@@ -548,9 +500,8 @@ def get_top_tracks(**keys):
@dbserver.route("/artistinfo")
def artistInfo_external():
keys = FormsDict.decode(request.query)
@dbserver.get("artistinfo")
def artistInfo_external(**keys):
k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter}
@@ -560,14 +511,16 @@ def artistInfo_external():
def artistInfo(artist):
charts = db_aggregate(by="ARTIST")
scrobbles = len(db_query(artists=[artist])) #we cant take the scrobble number from the charts because that includes all countas scrobbles
scrobbles = len(db_query(artists=[artist]))
#we cant take the scrobble number from the charts because that includes all countas scrobbles
try:
c = [e for e in charts if e["artist"] == artist][0]
others = [a for a in coa.getAllAssociated(artist) if a in ARTISTS]
position = c["rank"]
return {"scrobbles":scrobbles,"position":position,"associated":others,"medals":MEDALS.get(artist)}
except:
# if the artist isnt in the charts, they are not being credited and we need to show information about the credited one
# if the artist isnt in the charts, they are not being credited and we
# need to show information about the credited one
artist = coa.getCredited(artist)
c = [e for e in charts if e["artist"] == artist][0]
position = c["rank"]
@@ -577,23 +530,37 @@ def artistInfo(artist):
@dbserver.route("/trackinfo")
def trackInfo_external():
keys = FormsDict.decode(request.query)
@dbserver.get("trackinfo")
def trackInfo_external(artist:Multi[str],**keys):
# transform into a multidict so we can use our nomral uri_to_internal function
keys = FormsDict(keys)
for a in artist:
keys.append("artist",a)
k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True)
ckeys = {**k_filter}
results = trackInfo(**ckeys)
return results
def trackInfo(artists,title):
def trackInfo(track):
charts = db_aggregate(by="TRACK")
#scrobbles = len(db_query(artists=artists,title=title)) #chart entry of track always has right scrobble number, no countas rules here
c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0]
#c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0]
c = [e for e in charts if e["track"] == track][0]
scrobbles = c["scrobbles"]
position = c["rank"]
cert = None
threshold_gold, threshold_platinum, threshold_diamond = settings.get_settings("SCROBBLES_GOLD","SCROBBLES_PLATINUM","SCROBBLES_DIAMOND")
if scrobbles >= threshold_diamond: cert = "diamond"
elif scrobbles >= threshold_platinum: cert = "platinum"
elif scrobbles >= threshold_gold: cert = "gold"
return {"scrobbles":scrobbles,"position":position,"medals":MEDALS_TRACKS.get((frozenset(artists),title))}
return {
"scrobbles":scrobbles,
"position":position,
"medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])),
"certification":cert
}
@@ -601,9 +568,8 @@ def trackInfo(artists,title):
@dbserver.get("/newscrobble")
def pseudo_post_scrobble():
keys = FormsDict.decode(request.query) # The Dal★Shabet handler
@dbserver.get("newscrobble")
def pseudo_post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
@@ -626,9 +592,8 @@ def pseudo_post_scrobble():
return {"status":"success","track":trackdict}
@dbserver.post("/newscrobble")
def post_scrobble():
keys = FormsDict.decode(request.forms) # The Dal★Shabet handler
@dbserver.post("newscrobble")
def post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
@@ -659,28 +624,26 @@ def post_scrobble():
# standard-compliant scrobbling methods
@dbserver.post("/s/<path:path>")
@dbserver.get("/s/<path:path>")
def sapi(path):
path = path.split("/")
@dbserver.post("s/{path}",pass_headers=True)
@dbserver.get("s/{path}",pass_headers=True)
def sapi(path:Multi,**keys):
"""Scrobbles according to a standardized protocol.
:param string path: Path according to the scrobble protocol
:param string keys: Query keys according to the scrobble protocol
"""
path = list(filter(None,path))
headers = request.headers
if request.get_header("Content-Type") is not None and "application/json" in request.get_header("Content-Type"):
keys = request.json
else:
keys = FormsDict.decode(request.params)
auth = request.auth
return compliant_api.handle(path,keys,headers,auth)
return compliant_api.handle(path,keys)
@dbserver.route("/sync")
@dbserver.get("sync")
def abouttoshutdown():
sync()
#sys.exit()
@dbserver.post("/newrule")
@dbserver.post("newrule")
def newrule():
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None)
@@ -691,7 +654,7 @@ def newrule():
db_rulestate = False
@dbserver.route("/issues")
@dbserver.get("issues")
def issues_external(): #probably not even needed
return issues()
@@ -787,7 +750,7 @@ def issues():
return {"duplicates":duplicates,"combined":combined,"newartists":newartists,"inconsistent":inconsistent}
@dbserver.post("/importrules")
@dbserver.post("importrules")
def import_rulemodule():
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None)
@@ -807,7 +770,7 @@ def import_rulemodule():
@dbserver.post("/rebuild")
@dbserver.post("rebuild")
def rebuild():
keys = FormsDict.decode(request.forms)
@@ -827,9 +790,8 @@ def rebuild():
@dbserver.get("/search")
def search():
keys = FormsDict.decode(request.query)
@dbserver.get("search")
def search(**keys):
query = keys.get("query")
max_ = keys.get("max")
if max_ is not None: max_ = int(max_)
@@ -923,10 +885,10 @@ def build_db():
# coa.updateIDs(ARTISTS)
#start regular tasks
update_medals()
utilities.update_medals()
global db_rulestate
db_rulestate = consistentRulestate("scrobbles",cla.checksums)
db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums)
log("Database fully built!")
@@ -959,7 +921,7 @@ def sync():
for e in entries:
tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False)
#addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False)
combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
global lastsync
@@ -989,7 +951,7 @@ cacheday = (0,0,0)
def db_query(**kwargs):
check_cache_age()
global cache_query, cache_query_permanent
key = serialize(kwargs)
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_query_permanent:
#print("Hit")
@@ -1014,7 +976,7 @@ else:
def db_aggregate(**kwargs):
check_cache_age()
global cache_aggregate, cache_aggregate_permanent
key = serialize(kwargs)
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_aggregate_permanent: return copy.copy(cache_aggregate_permanent.get(key))
result = db_aggregate_full(**kwargs)

View File

@@ -5,53 +5,60 @@ from doreah.settings import get_settings
from doreah.logging import log
apis_artists = [
{
apis_artists = []
if get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"]:
apis_artists.append({
"name":"LastFM + Fanart.tv",
"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"],
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"],
"steps":[
("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"),
("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + str(get_settings("LASTFM_API_KEY")) + "&format=json"),
("parse",["artist","mbid"]),
("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + get_settings("FANARTTV_API_KEY")),
("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + str(get_settings("FANARTTV_API_KEY"))),
("parse",["artistthumb",0,"url"])
]
},
{
})
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
apis_artists.append({
"name":"Spotify",
"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
"steps":[
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
("parse",["access_token"]),
("get","https://api.spotify.com/v1/search?q={artiststring}&type=artist&access_token={var}"),
("parse",["artists","items",0,"images",0,"url"])
]
}
]
})
apis_tracks = [
{
apis_tracks = []
if get_settings("LASTFM_API_KEY") not in [None,"ASK"]:
apis_tracks.append({
"name":"LastFM",
"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"],
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"],
"steps":[
("get","https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={titlestring}&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"),
("parse",["track","album","image",3,"#text"])
]
},
{
})
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
apis_tracks.append({
"name":"Spotify",
"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
"steps":[
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
("parse",["access_token"]),
("get","https://api.spotify.com/v1/search?q={artiststring}%20{titlestring}&type=track&access_token={var}"),
("parse",["tracks","items",0,"album","images",0,"url"])
]
}
]
})
def api_request_artist(artist):
for api in apis_artists:
if api["check"]:
if True:
log("API: " + api["name"] + "; Image request: " + artist,module="external")
try:
artiststring = urllib.parse.quote(artist)
@@ -85,7 +92,7 @@ def api_request_artist(artist):
def api_request_track(track):
artists, title = track
for api in apis_tracks:
if api["check"]:
if True:
log("API: " + api["name"] + "; Image request: " + "/".join(artists) + " - " + title,module="external")
try:
artiststring = urllib.parse.quote(", ".join(artists))

View File

@@ -14,8 +14,9 @@ def entity_column(element,counting=[],image=None):
if "artists" in element:
# track
html += "<td class='artists'>" + html_links(element["artists"]) + "</td>"
html += "<td class='title'>" + html_link(element) + "</td>"
# html += "<td class='artists'>" + html_links(element["artists"]) + "</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>"
else:
# artist
html += "<td class='artist'>" + html_link(element)

9
maloja
View File

@@ -13,7 +13,8 @@ neededmodules = [
"bottle",
"waitress",
"setproctitle",
"doreah"
"doreah",
"nimrodel"
]
recommendedmodules = [
@@ -117,10 +118,10 @@ def install():
if toinstall != [] or toinstallr != []:
if os.geteuid() != 0:
print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically. For this, you need to run this script as a root user.")
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically. For this, you need to run this script as a root user.")
return False
else:
print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.")
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.")
fail = False
if toinstall != []:
print("Attempt to install required modules? [Y/n]")
@@ -283,7 +284,7 @@ def update():
os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR)
print("Make sure to install the latest version of doreah! (" + yellow("pip3 install --upgrade --no-cache-dir doreah") + ")")
print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")")
if stop(): start() #stop returns whether it was running before, in which case we restart it

View File

@@ -66,6 +66,9 @@ class MRangeDescriptor:
def unlimited(self):
return False
def active(self):
return (self.last_stamp() > datetime.datetime.utcnow().timestamp())
# returns the description of the range including buttons to go back and forth
#def desc_interactive(self,**kwargs):
# if self.next(1) is None:
@@ -99,7 +102,7 @@ class MTime(MRangeDescriptor):
# whether we currently live or will ever again live in this range
def active(self):
tod = datetime.date.today()
tod = datetime.datetime.utcnow().date()
if tod.year > self.year: return False
if self.precision == 1: return True
if tod.year == self.year:
@@ -235,13 +238,13 @@ class MTimeWeek(MRangeDescriptor):
return str(self)
# whether we currently live or will ever again live in this range
def active(self):
tod = datetime.date.today()
if tod.year > self.year: return False
if tod.year == self.year:
if tod.chrcalendar()[1] > self.week: return False
return True
# def active(self):
# tod = datetime.date.today()
# if tod.year > self.year: return False
# if tod.year == self.year:
# if tod.chrcalendar()[1] > self.week: return False
#
# return True
def urikeys(self):
return {"in":str(self)}

6
requirements.txt Normal file
View File

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

View File

@@ -5,6 +5,7 @@
countas Selena Gomez & The Scene Selena Gomez
countas The Police Sting
countas Trouble Maker HyunA
countas S Club 7 Tina Barrett
# Group more famous than single artist
countas RenoakRhythm Approaching Nirvana
countas Shirley Manson Garbage
1 # NAME: Artists and Groups
5 countas
6 countas
7 # Group more famous than single artist countas
8 # Group more famous than single artist
9 countas
10 countas
11 countas

View File

@@ -46,6 +46,9 @@ replacetitle 종이 심장 (Paper Heart) Paper Heart
replacetitle 나비 (Butterfly) Butterfly
replacetitle Déjà vu Déjà Vu
replacetitle 라차타 (LA chA TA) LA chA TA
replacetitle 여우 같은 내 친구 (No More) No More
replacetitle 시그널 (Signal) Signal
replacetitle 미행 (그림자 : Shadow) Shadow
# Stellar
replaceartist STELLAR Stellar
@@ -111,6 +114,7 @@ replacetitle 음오아예 (Um Oh Ah Yeh) (Um Oh Ah Yeh) Um Oh Ah Yeh
replacetitle 따끔 (a little bit) A Little Bit
# Hello Venus
replaceartist HELLOVENUS Hello Venus
replaceartist Hello/Venus Hello Venus
# BESTie
1 # NAME: K-Pop Girl Groups
46 replaceartist replacetitle
47 replaceartist replacetitle
48 replaceartist # Stellar
49 replaceartist
50 replaceartist
51 replaceartist
52 replacetitle
53 # Red Velvet
54 countas
114 # Laysha replaceartist
115 replaceartist # Laysha
116 replacetitle replaceartist
117 replacetitle
118 # GFriend
119 replaceartist
120 # Girl's Generation

View File

@@ -20,3 +20,4 @@ notanartist Aniron Theme For Aragorn And Arwen
notanartist Lament for Gandalf
replaceartist James Galway Sir James Galway
replaceartist Ben del Maestro Ben Del Maestro
replacetitle Anduril Andúril
1 # NAME: Lord of the Rings Soundtrack
20 replaceartist
21 replaceartist
22 replacetitle
23

View File

@@ -202,7 +202,8 @@ class Controller {
// Already played full song
while (this.alreadyPlayed > this.currentLength) {
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
scrobble(this.currentArtist,this.currentTitle,this.currentLength)
var secondsago = this.alreadyPlayed
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
}
this.setUpdate()
@@ -248,7 +249,8 @@ class Controller {
// Already played full song
while (this.alreadyPlayed > this.currentLength) {
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
scrobble(this.currentArtist,this.currentTitle,this.currentLength)
var secondsago = this.alreadyPlayed
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
}
this.currentlyPlaying = false
@@ -282,17 +284,22 @@ class Controller {
function scrobble(artist,title,seconds) {
console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime")
artiststring = encodeURIComponent(artist)
titlestring = encodeURIComponent(title)
function scrobble(artist,title,seconds,secondsago=0) {
console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime, " + secondsago + " seconds ago")
var artiststring = encodeURIComponent(artist)
var titlestring = encodeURIComponent(title)
var d = new Date()
var time = Math.floor(d.getTime()/1000) - secondsago
//console.log("Time: " + time)
var requestbody = "artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&time=" + time
chrome.storage.local.get("apikey",function(result) {
APIKEY = result["apikey"]
chrome.storage.local.get("serverurl",function(result) {
URL = result["serverurl"]
var xhttp = new XMLHttpRequest();
xhttp.open("POST",URL + "/api/newscrobble",true);
xhttp.send("artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&key=" + APIKEY)
xhttp.send(requestbody + "&key=" + APIKEY)
//console.log("Sent: " + requestbody)
});
});

Binary file not shown.

View File

@@ -124,6 +124,7 @@ def static_image(pth):
@webserver.route("/<name:re:.*\\.png>")
@webserver.route("/<name:re:.*\\.jpeg>")
@webserver.route("/<name:re:.*\\.ico>")
@webserver.route("/<name:re:.*\\.txt>")
def static(name):
response = static_file("website/" + name,root="")
response.set_header("Cache-Control", "public, max-age=3600")
@@ -214,7 +215,8 @@ setproctitle.setproctitle("Maloja")
## start database
database.start_db()
database.register_subroutes(webserver,"/api")
#database.register_subroutes(webserver,"/api")
database.dbserver.mount(server=webserver)
log("Starting up Maloja server...")
run(webserver, host='::', port=MAIN_PORT, server='waitress')

View File

@@ -30,6 +30,13 @@ DEFAULT_RANGE_CHARTS_TRACKS = year
# can be days, weeks, months, years
DEFAULT_RANGE_PULSE = months
[Fluff]
# how many scrobbles a track needs to aquire this status
SCROBBLES_GOLD = 250
SCROBBLES_PLATINUM = 500
SCROBBLES_DIAMOND = 1000
[Misc]
EXPERIMENTAL_FEATURES = no

View File

@@ -375,6 +375,12 @@ a.bronze {
img.certrecord {
height:30px;
vertical-align: text-bottom;
}
/*
**
**
@@ -463,12 +469,16 @@ table.list td.icon div {
background-position:center;
}
table.list td.artists,td.artist,td.title {
table.list td.artists,td.artist,td.title,td.track {
min-width:100px;
}
table.list td.track span.artist_in_trackcolumn {
color:#bbb;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

2
website/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /api/

View File

@@ -15,7 +15,7 @@
</td>
<td class="text">
<span>KEY_ARTISTS</span><br/>
<h1>KEY_TRACKTITLE</h1> <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?max=100">KEY_POSITION</a></span>
<p class="stats"><a href="/scrobbles?KEY_SCROBBLELINK">KEY_SCROBBLES Scrobbles</a></p>

View File

@@ -16,11 +16,14 @@ def instructions(keys):
imgurl = getTrackImage(track["artists"],track["title"],fast=True)
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
data = database.trackInfo(track["artists"],track["title"])
data = database.trackInfo(track)
scrobblesnum = str(data["scrobbles"])
pos = "#" + str(data["position"])
html_cert = ""
if data["certification"] is not None:
html_cert = "<img class='certrecord' src='/media/record_{cert}.png' title='This track has reached {certc} status' />".format(cert=data["certification"],certc=data["certification"].capitalize())
html_medals = ""
if "medals" in data and data["medals"] is not None:
@@ -61,6 +64,7 @@ def instructions(keys):
"KEY_IMAGEURL":imgurl,
"KEY_SCROBBLELINK":compose_querystring(keys),
"KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert,
"KEY_SCROBBLELIST":html_scrobbles,
# pulse
"KEY_PULSE_MONTHS":html_pulse_months,