Merge branch 'version2'
12
.doreah
|
@ -1,12 +0,0 @@
|
|||
logging:
|
||||
logfolder: "logs"
|
||||
settings:
|
||||
files:
|
||||
- "settings/default.ini"
|
||||
- "settings/settings.ini"
|
||||
caching:
|
||||
folder: "cache/"
|
||||
regular:
|
||||
autostart: false
|
||||
pyhp:
|
||||
version: 2
|
|
@ -1,17 +1,10 @@
|
|||
# generic temporary / dev files
|
||||
*.pyc
|
||||
*.sh
|
||||
!/update_requirements.sh
|
||||
!/install_*.sh
|
||||
*.note
|
||||
*.xcf
|
||||
nohup.out
|
||||
/.dev
|
||||
|
||||
# user files
|
||||
*.tsv
|
||||
*.rulestate
|
||||
*.log
|
||||
*.css
|
||||
|
||||
# currently not using
|
||||
/screenshot*.png
|
||||
|
|
31
README.md
|
@ -1,5 +1,10 @@
|
|||
# Maloja
|
||||
|
||||
[![](https://img.shields.io/pypi/v/malojaserver?style=for-the-badge)](https://pypi.org/project/malojaserver/)
|
||||
[![](https://img.shields.io/pypi/dm/malojaserver?style=for-the-badge)](https://pypi.org/project/malojaserver/)
|
||||
[![](https://img.shields.io/github/stars/krateng/maloja?style=for-the-badge&color=purple)](https://github.com/krateng/maloja/stargazers)
|
||||
[![](https://img.shields.io/pypi/l/malojaserver?style=for-the-badge)](https://github.com/krateng/maloja/blob/master/LICENSE)
|
||||
|
||||
Simple self-hosted music scrobble database to create personal listening statistics. No recommendations, no social network, no nonsense.
|
||||
|
||||
You can check [my own Maloja page](https://maloja.krateng.ch) to see what it looks like.
|
||||
|
@ -8,7 +13,7 @@ You can check [my own Maloja page](https://maloja.krateng.ch) to see what it loo
|
|||
|
||||
**Update to Version 2**
|
||||
|
||||
With the update 2.0, Maloja has been refactored into a Python package and the old update script no longer works. I will keep this repository on the old version for a while so that users with regular updates have a chance to load the transition script. If you have any trouble with updating, simply install Maloja as described below, then manually copy all your user data to your `~/.local/share/maloja` folder.
|
||||
With the update 2.0, Maloja has been refactored into a Python package and the old update script no longer works. If you're still on version 1, simply install Maloja as described below, then manually copy all your user data to your `~/.local/share/maloja` folder.
|
||||
|
||||
## Why not Last.fm / Libre.fm / GNU FM?
|
||||
|
||||
|
@ -22,15 +27,12 @@ Also neat: You can use your **custom artist or track images**.
|
|||
|
||||
## Requirements
|
||||
|
||||
* Python 3
|
||||
* Pip packages specified in `requirements.txt`
|
||||
* 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!
|
||||
* Python 3.5 or higher
|
||||
* 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/) (you need a project key, not a personal one). These are free of charge!
|
||||
|
||||
## How to install
|
||||
|
||||
1) Install Maloja with
|
||||
|
||||
pip3 install malojaserver
|
||||
1) Download Maloja with the command `pip install malojaserver`. Make sure to use the correct python version (Use `pip3` if necessary). I've provided a simple .sh file to get Maloja going on an Alpine or Ubuntu server / container.
|
||||
|
||||
2) Start the server with
|
||||
|
||||
|
@ -51,11 +53,20 @@ Also neat: You can use your **custom artist or track images**.
|
|||
maloja stop
|
||||
maloja restart
|
||||
maloja start
|
||||
maloja update
|
||||
|
||||
3) Various folders have `.info` files with more information on how to use their associated features.
|
||||
3) Update Maloja with `pip install malojaserver --upgrade --no-cache-dir`
|
||||
|
||||
4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
|
||||
4) Various folders have `.info` files with more information on how to use their associated features.
|
||||
|
||||
5) If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
|
||||
|
||||
6) To backup your data, run
|
||||
|
||||
maloja backup
|
||||
|
||||
or, to only backup essential data (no artwork etc)
|
||||
|
||||
maloja backup -l minimal
|
||||
|
||||
## How to scrobble
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.info
|
|
@ -1 +0,0 @@
|
|||
!example_file.tsv
|
|
@ -1,46 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
from cleanup import CleanerAgent
|
||||
from doreah.logging import log
|
||||
import difflib
|
||||
|
||||
wendigo = CleanerAgent()
|
||||
|
||||
exp = r"([0-9]*)(\t+)([^\t]+?)(\t+)([^\t]+)(\t*)([^\t]*)\n"
|
||||
|
||||
for fn in os.listdir("scrobbles/"):
|
||||
if fn.endswith(".tsv"):
|
||||
f = open("scrobbles/" + fn)
|
||||
fnew = open("scrobbles/" + fn + "_new","w")
|
||||
for l in f:
|
||||
|
||||
a,t = re.sub(exp,r"\3",l), re.sub(exp,r"\5",l)
|
||||
r1,r2,r3 = re.sub(exp,r"\1\2",l),re.sub(exp,r"\4",l),re.sub(exp,r"\6\7",l)
|
||||
|
||||
a = a.replace("␟",";")
|
||||
|
||||
(al,t) = wendigo.fullclean(a,t)
|
||||
a = "␟".join(al)
|
||||
fnew.write(r1 + a + r2 + t + r3 + "\n")
|
||||
|
||||
#print("Artists: " + a)
|
||||
#print("Title: " + t)
|
||||
#print("1: " + r1)
|
||||
#print("2: " + r2)
|
||||
#print("3: " + r3)
|
||||
|
||||
f.close()
|
||||
fnew.close()
|
||||
|
||||
#os.system("diff " + "scrobbles/" + fn + "_new" + " " + "scrobbles/" + fn)
|
||||
with open("scrobbles/" + fn + "_new","r") as newfile:
|
||||
with open("scrobbles/" + fn,"r") as oldfile:
|
||||
diff = difflib.unified_diff(oldfile.read().split("\n"),newfile.read().split("\n"),lineterm="")
|
||||
diff = list(diff)[2:]
|
||||
log("Diff for scrobbles/" + fn + "".join("\n\t" + d for d in diff),module="fixer")
|
||||
|
||||
os.rename("scrobbles/" + fn + "_new","scrobbles/" + fn)
|
||||
|
||||
checkfile = open("scrobbles/" + fn + ".rulestate","w")
|
||||
checkfile.write(wendigo.checksums)
|
||||
checkfile.close()
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.info
|
10
info.py
|
@ -1,10 +0,0 @@
|
|||
import os
|
||||
|
||||
author = {
|
||||
"name":"Johannes Krattenmacher",
|
||||
"email":"maloja@krateng.dev",
|
||||
"github": "krateng"
|
||||
}
|
||||
version = 1,5,16
|
||||
versionstr = ".".join(str(n) for n in version)
|
||||
dev = os.path.exists("./.dev")
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
apk add python3 python3-dev gcc libxml2-dev libxslt-dev py3-pip libc-dev
|
||||
pip3 install malojaserver
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
apt update
|
||||
apt install python3 python3-pip
|
||||
pip3 install malojaserver
|
|
@ -1,63 +0,0 @@
|
|||
import sys, os, datetime, re, cleanup
|
||||
from cleanup import *
|
||||
from utilities import *
|
||||
|
||||
|
||||
log = open(sys.argv[1])
|
||||
outputlog = open(sys.argv[2],"w")
|
||||
checksumfile = open(sys.argv[2] + ".rulestate","w") #this file stores an identifier for all rules that were in place when the corresponding file was created
|
||||
|
||||
|
||||
c = CleanerAgent()
|
||||
stamps = [99999999999999]
|
||||
|
||||
for l in log:
|
||||
l = l.replace("\n","")
|
||||
data = l.split(",")
|
||||
|
||||
artist = data[0]
|
||||
album = data[1]
|
||||
title = data[2]
|
||||
time = data[3]
|
||||
|
||||
|
||||
(artists,title) = c.fullclean(artist,title)
|
||||
|
||||
artistsstr = "␟".join(artists)
|
||||
|
||||
|
||||
timeparts = time.split(" ")
|
||||
(h,m) = timeparts[3].split(":")
|
||||
|
||||
months = {"Jan":1,"Feb":2,"Mar":3,"Apr":4,"May":5,"Jun":6,"Jul":7,"Aug":8,"Sep":9,"Oct":10,"Nov":11,"Dec":12}
|
||||
|
||||
timestamp = int(datetime.datetime(int(timeparts[2]),months[timeparts[1]],int(timeparts[0]),int(h),int(m)).timestamp())
|
||||
|
||||
|
||||
## We prevent double timestamps in the database creation, so we technically don't need them in the files
|
||||
## however since the conversion from lastfm to maloja is a one-time thing, we should take any effort to make the file as good as possible
|
||||
if (timestamp < stamps[-1]):
|
||||
pass
|
||||
elif (timestamp == stamps[-1]):
|
||||
timestamp -= 1
|
||||
else:
|
||||
while(timestamp in stamps):
|
||||
timestamp -= 1
|
||||
|
||||
if (timestamp < stamps[-1]):
|
||||
stamps.append(timestamp)
|
||||
else:
|
||||
stamps.insert(0,timestamp)
|
||||
|
||||
|
||||
entry = "\t".join([str(timestamp),artistsstr,title,album])
|
||||
entry = entry.replace("#",r"\num")
|
||||
|
||||
outputlog.write(entry)
|
||||
outputlog.write("\n")
|
||||
|
||||
checksumfile.write(c.checksums)
|
||||
|
||||
log.close()
|
||||
outputlog.close()
|
||||
checksumfile.close()
|
383
maloja
|
@ -1,383 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import stat
|
||||
import pathlib
|
||||
|
||||
|
||||
|
||||
neededmodules = [
|
||||
"bottle",
|
||||
"waitress",
|
||||
"setproctitle",
|
||||
"doreah",
|
||||
"nimrodel"
|
||||
]
|
||||
|
||||
recommendedmodules = [
|
||||
"wand"
|
||||
]
|
||||
|
||||
SOURCE_URL = "https://github.com/krateng/maloja/archive/master.zip"
|
||||
|
||||
|
||||
|
||||
def blue(txt): return "\033[94m" + txt + "\033[0m"
|
||||
def green(txt): return "\033[92m" + txt + "\033[0m"
|
||||
def yellow(txt): return "\033[93m" + txt + "\033[0m"
|
||||
|
||||
## GOTODIR goes to directory that seems to have a maloja install
|
||||
## SETUP assumes correct directory. sets settings and key
|
||||
## INSTALL ignores local files, just installs prerequisites
|
||||
## START INSTALL - GOTODIR - SETUP - starts process
|
||||
## RESTART STOP - START
|
||||
## STOP Stops process
|
||||
## UPDATE GOTODIR - updates from repo
|
||||
## LOADLASTFM GOTODIR - imports csv data
|
||||
## INSTALLHERE makes this directory valid - UPDATE - INSTALL - SETUP
|
||||
|
||||
|
||||
|
||||
def update_version_2():
|
||||
|
||||
|
||||
|
||||
if gotodir():
|
||||
try:
|
||||
DATA_DIR = os.environ["XDG_DATA_HOME"].split(":")[0]
|
||||
assert os.path.exists(DATA_DIR)
|
||||
except:
|
||||
DATA_DIR = os.path.join(os.environ["HOME"],".local/share/")
|
||||
|
||||
DATA_DIR = os.path.join(DATA_DIR,"maloja")
|
||||
os.makedirs(DATA_DIR,exist_ok=True)
|
||||
|
||||
print(yellow("With version 2.0, Maloja has been refactored into a python package. The updater will attempt to make this transition smooth."))
|
||||
print("Relocating user data...")
|
||||
|
||||
import shutil
|
||||
for folder in ["clients","images","logs","rules","scrobbles","settings"]:
|
||||
shutil.copytree(folder,os.path.join(DATA_DIR,folder))
|
||||
|
||||
print("Installing pip package...")
|
||||
|
||||
os.system("pip3 install malojaserver --upgrade --no-cache-dir")
|
||||
|
||||
print(yellow("Maloja may now be started from any directory with the command"),blue("maloja start"))
|
||||
print(yellow("Updates will continue to work with ") + blue("maloja update") + yellow(", but you may also use pip directly"))
|
||||
print(yellow("Please test your new server installation. If it works correctly with all your scrobbles, rules, settings and custom images, you can delete your old Maloja directory."))
|
||||
|
||||
if stop(): os.system("maloja start")
|
||||
|
||||
def gotodir():
|
||||
if os.path.exists("./server.py"):
|
||||
return True
|
||||
elif os.path.exists("/opt/maloja/server.py"):
|
||||
os.chdir("/opt/maloja/")
|
||||
return True
|
||||
|
||||
print("Maloja installation could not be found.")
|
||||
return False
|
||||
|
||||
def setup():
|
||||
|
||||
from doreah import settings
|
||||
|
||||
# EXTERNAL API KEYS
|
||||
apikeys = {
|
||||
"LASTFM_API_KEY":"Last.fm API Key",
|
||||
"FANARTTV_API_KEY":"Fanart.tv API Key",
|
||||
"SPOTIFY_API_ID":"Spotify Client ID",
|
||||
"SPOTIFY_API_SECRET":"Spotify Client Secret"
|
||||
}
|
||||
|
||||
print("Various external services can be used to display images. If not enough of them are set up, only local images will be used.")
|
||||
for k in apikeys:
|
||||
key = settings.get_settings(k)
|
||||
if key is None:
|
||||
print("\t" + "Currently not using a " + apikeys[k] + " for image display.")
|
||||
elif key == "ASK":
|
||||
print("\t" + "Please enter your " + apikeys[k] + ". If you do not want to use one at this moment, simply leave this empty and press Enter.")
|
||||
key = input()
|
||||
if key == "": key = None
|
||||
settings.update_settings("settings/settings.ini",{k:key},create_new=True)
|
||||
else:
|
||||
print("\t" + apikeys[k] + " found.")
|
||||
|
||||
|
||||
# OWN API KEY
|
||||
if os.path.exists("./clients/authenticated_machines.tsv"):
|
||||
pass
|
||||
else:
|
||||
print("Do you want to set up a key to enable scrobbling? Your scrobble extension needs that key so that only you can scrobble tracks to your database. [Y/n]")
|
||||
answer = input()
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
import random
|
||||
key = ""
|
||||
for i in range(64):
|
||||
key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
|
||||
print("Your API Key: " + yellow(key))
|
||||
with open("./clients/authenticated_machines.tsv","w") as keyfile:
|
||||
keyfile.write(key + "\t" + "Default Generated Key")
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
pass
|
||||
|
||||
def install():
|
||||
toinstall = []
|
||||
toinstallr = []
|
||||
for m in neededmodules:
|
||||
try:
|
||||
exec("import " + m) #I'm sorry
|
||||
except:
|
||||
toinstall.append(m)
|
||||
|
||||
for m in recommendedmodules:
|
||||
try:
|
||||
exec("import " + m)
|
||||
except:
|
||||
toinstallr.append(m)
|
||||
|
||||
if toinstall != []:
|
||||
print("The following python modules need to be installed:")
|
||||
for m in toinstall:
|
||||
print("\t" + yellow(m))
|
||||
if toinstallr != []:
|
||||
print("The following python modules are highly recommended, some features will not work without them:")
|
||||
for m in toinstallr:
|
||||
print("\t" + yellow(m))
|
||||
|
||||
if toinstall != [] or toinstallr != []:
|
||||
if os.geteuid() != 0:
|
||||
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("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]")
|
||||
answer = input()
|
||||
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
for m in toinstall:
|
||||
try:
|
||||
print("Installing " + m + " with pip...")
|
||||
from pip._internal import main as pipmain
|
||||
#os.system("pip3 install " + m)
|
||||
pipmain(["install",m])
|
||||
print("Success!")
|
||||
except:
|
||||
print("Failure!")
|
||||
fail = True
|
||||
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
return False #if you dont want to auto install required, you probably dont want to install recommended
|
||||
else:
|
||||
print("What?")
|
||||
return False
|
||||
if toinstallr != []:
|
||||
print("Attempt to install recommended modules? [Y/n]")
|
||||
answer = input()
|
||||
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
for m in toinstallr:
|
||||
try:
|
||||
print("Installing " + m + " with pip...")
|
||||
from pip._internal import main as pipmain
|
||||
#os.system("pip3 install " + m)
|
||||
pipmain(["install",m])
|
||||
print("Success!")
|
||||
except:
|
||||
print("Failure!")
|
||||
fail = True
|
||||
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
return False
|
||||
else:
|
||||
print("What?")
|
||||
return False
|
||||
|
||||
if fail: return False
|
||||
print("All modules successfully installed!")
|
||||
print("Run the script again (without root) to start Maloja.")
|
||||
return False
|
||||
|
||||
else:
|
||||
print("All necessary modules seem to be installed.")
|
||||
return True
|
||||
|
||||
def getInstance():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","Maloja"])
|
||||
pid = int(output)
|
||||
return pid
|
||||
except:
|
||||
return None
|
||||
|
||||
def getInstanceSupervisor():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","maloja_supervisor"])
|
||||
pid = int(output)
|
||||
return pid
|
||||
except:
|
||||
return None
|
||||
|
||||
def start():
|
||||
if install():
|
||||
|
||||
if gotodir():
|
||||
setup()
|
||||
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))
|
||||
|
||||
from doreah import settings
|
||||
port = settings.get_settings("WEB_PORT")
|
||||
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /setup to get started.")
|
||||
print("If you're installing this on your local machine, these links should get you there:")
|
||||
print("\t" + blue("http://localhost:" + str(port)))
|
||||
print("\t" + blue("http://localhost:" + str(port) + "/setup"))
|
||||
return True
|
||||
#else:
|
||||
# os.chdir("/opt/maloja/")
|
||||
# p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
# print("Maloja started! PID: " + str(p.pid))
|
||||
# return True
|
||||
|
||||
print("Error while starting Maloja.")
|
||||
return False
|
||||
|
||||
def restart():
|
||||
#pid = getInstance()
|
||||
#if pid == None:
|
||||
# print("Server is not running.")
|
||||
#else:
|
||||
# stop()
|
||||
#start()
|
||||
|
||||
wasrunning = stop()
|
||||
start()
|
||||
return wasrunning
|
||||
|
||||
def stop():
|
||||
pid_sv = getInstanceSupervisor()
|
||||
if pid_sv is not None:
|
||||
os.kill(pid_sv,signal.SIGTERM)
|
||||
|
||||
pid = getInstance()
|
||||
if pid is None:
|
||||
print("Server is not running")
|
||||
return False
|
||||
else:
|
||||
os.kill(pid,signal.SIGTERM)
|
||||
print("Maloja stopped! PID: " + str(pid))
|
||||
return True
|
||||
|
||||
def update():
|
||||
|
||||
import urllib.request
|
||||
import shutil
|
||||
#import tempfile
|
||||
import zipfile
|
||||
import distutils.dir_util
|
||||
|
||||
if not gotodir(): return False
|
||||
|
||||
if os.path.exists("./.dev"):
|
||||
print("Better not overwrite the development server!")
|
||||
return
|
||||
|
||||
print("Updating Maloja...")
|
||||
#with urllib.request.urlopen(SOURCE_URL) as response:
|
||||
# with tempfile.NamedTemporaryFile(delete=True) as tmpfile:
|
||||
# shutil.copyfileobj(response,tmpfile)
|
||||
#
|
||||
# with zipfile.ZipFile(tmpfile.name,"r") as z:
|
||||
#
|
||||
# for f in z.namelist():
|
||||
# #print("extracting " + f)
|
||||
# z.extract(f)
|
||||
|
||||
|
||||
os.system("wget " + SOURCE_URL)
|
||||
with zipfile.ZipFile("./master.zip","r") as z:
|
||||
|
||||
# if we ever have a separate directory for the code
|
||||
# (the calling update script is not the same version as the current
|
||||
# remote repository, so we better add this check just in case)
|
||||
if "source/" in z.namelist():
|
||||
for f in z.namelist():
|
||||
if f.startswith("source/"):
|
||||
z.extract(f)
|
||||
for dir,_,files in os.walk("source"):
|
||||
for f in files:
|
||||
origfile = os.path.join(dir,f)
|
||||
newfile = ps.path.join(dir[7:],f)
|
||||
os.renames(origfile,newfile) #also prunes empty directory
|
||||
else:
|
||||
for f in z.namelist():
|
||||
z.extract(f)
|
||||
|
||||
os.remove("./master.zip")
|
||||
|
||||
|
||||
distutils.dir_util.copy_tree("./maloja-master/","./",verbose=2)
|
||||
shutil.rmtree("./maloja-master")
|
||||
print("Done!")
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
def loadlastfm():
|
||||
|
||||
try:
|
||||
filename = sys.argv[2]
|
||||
filename = os.path.abspath(filename)
|
||||
except:
|
||||
print("Please specify a file!")
|
||||
return
|
||||
|
||||
if gotodir():
|
||||
if os.path.exists("./scrobbles/lastfmimport.tsv"):
|
||||
print("Already imported Last.FM data. Overwrite? [y/N]")
|
||||
if input().lower() in ["y","yes","yea","1","positive","true"]:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
print("Please wait...")
|
||||
os.system("python3 ./lastfmconverter.py " + filename + " ./scrobbles/lastfmimport.tsv")
|
||||
print("Successfully imported your Last.FM scrobbles!")
|
||||
|
||||
def installhere():
|
||||
if len(os.listdir()) > 1:
|
||||
print("You should install Maloja in an empty directory.")
|
||||
return False
|
||||
else:
|
||||
open("server.py","w").close()
|
||||
# if it's cheese, but it works, it ain't cheese
|
||||
update()
|
||||
install()
|
||||
setup()
|
||||
|
||||
print("Maloja installed! Start with " + yellow("./maloja start"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "start": restart()
|
||||
elif sys.argv[1] == "restart": restart()
|
||||
elif sys.argv[1] == "stop": stop()
|
||||
elif sys.argv[1] == "update": update_version_2()
|
||||
elif sys.argv[1] == "import": loadlastfm()
|
||||
elif sys.argv[1] == "install": installhere()
|
||||
else: print("Valid commands: start restart stop update import install")
|
|
@ -0,0 +1,35 @@
|
|||
### PACKAGE DATA
|
||||
|
||||
name = "maloja"
|
||||
desc = "Self-hosted music scrobble database"
|
||||
author = {
|
||||
"name":"Johannes Krattenmacher",
|
||||
"email":"maloja@krateng.dev",
|
||||
"github": "krateng"
|
||||
}
|
||||
version = 2,2,3
|
||||
versionstr = ".".join(str(n) for n in version)
|
||||
|
||||
|
||||
requires = [
|
||||
"bottle>=0.12.16",
|
||||
"waitress>=1.3",
|
||||
"doreah>=1.4.5",
|
||||
"nimrodel>=0.6.3",
|
||||
"setproctitle>=1.1.10",
|
||||
"wand>=0.5.4",
|
||||
"lesscpy>=0.13"
|
||||
]
|
||||
resources = [
|
||||
"web/*/*",
|
||||
"web/*",
|
||||
"static/*/*",
|
||||
"data_files/*/*",
|
||||
"data_files/*/*/*"
|
||||
]
|
||||
|
||||
commands = {
|
||||
"maloja":"controller:main"
|
||||
}
|
||||
|
||||
from . import globalconf
|
|
@ -0,0 +1,35 @@
|
|||
import tarfile
|
||||
from datetime import datetime
|
||||
import glob
|
||||
import os
|
||||
from .globalconf import datadir
|
||||
|
||||
|
||||
user_files = {
|
||||
"minimal":[
|
||||
"rules/*.tsv",
|
||||
"scrobbles"
|
||||
],
|
||||
"full":[
|
||||
"clients/authenticated_machines.tsv",
|
||||
"images/artists",
|
||||
"images/tracks",
|
||||
"settings/settings.ini"
|
||||
]
|
||||
}
|
||||
|
||||
def backup(folder,level="full"):
|
||||
|
||||
selected_files = user_files["minimal"] if level == "minimal" else user_files["minimal"] + user_files["full"]
|
||||
real_files = []
|
||||
for g in selected_files:
|
||||
real_files += glob.glob(datadir(g))
|
||||
|
||||
now = datetime.utcnow()
|
||||
timestr = now.strftime("%Y_%m_%d_%H_%M_%S")
|
||||
filename = "maloja_backup_" + timestr + ".tar.gz"
|
||||
archivefile = os.path.join(folder,filename)
|
||||
assert not os.path.exists(archivefile)
|
||||
with tarfile.open(name=archivefile,mode="x:gz") as archive:
|
||||
for f in real_files:
|
||||
archive.add(f)
|
|
@ -1,6 +1,8 @@
|
|||
import re
|
||||
import utilities
|
||||
from . import utilities
|
||||
from doreah import tsv, settings
|
||||
from .globalconf import datadir
|
||||
import pkg_resources
|
||||
|
||||
# need to do this as a class so it can retain loaded settings from file
|
||||
# apparently this is not true
|
||||
|
@ -11,21 +13,31 @@ class CleanerAgent:
|
|||
self.updateRules()
|
||||
|
||||
def updateRules(self):
|
||||
raw = tsv.parse_all("rules","string","string","string","string")
|
||||
raw = tsv.parse_all(datadir("rules"),"string","string","string","string")
|
||||
self.rules_belongtogether = [b for [a,b,c,d] in raw if a=="belongtogether"]
|
||||
self.rules_notanartist = [b for [a,b,c,d] in raw if a=="notanartist"]
|
||||
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,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_fixartists = {c.lower():b for [a,b,c,d] in raw if a=="fixartists"}
|
||||
self.rules_artistintitle = {b.lower():c for [a,b,c,d] in raw if a=="artistintitle"}
|
||||
#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
|
||||
self.checksums = utilities.checksumTSV("rules")
|
||||
#self.plugin_artistparsers = []
|
||||
#self.plugin_titleparsers = []
|
||||
#if settings.get_settings("USE_PARSE_PLUGINS"):
|
||||
# for ep in pkg_resources.iter_entry_points(group='maloja.artistparsers'):
|
||||
# self.plugin_artistparsers.append(ep.load())
|
||||
# for ep in pkg_resources.iter_entry_points(group='maloja.titleparsers'):
|
||||
# self.plugin_titleparsers.append(ep.load())
|
||||
|
||||
|
||||
# we always need to be able to tell if our current database is made with the current rules
|
||||
self.checksums = utilities.checksumTSV(datadir("rules"))
|
||||
|
||||
|
||||
def fullclean(self,artist,title):
|
||||
artists = self.parseArtists(self.removespecial(artist))
|
||||
|
@ -38,6 +50,11 @@ class CleanerAgent:
|
|||
allartists = allartists.split("␟")
|
||||
if set(reqartists).issubset(set(a.lower() for a in artists)):
|
||||
artists += allartists
|
||||
elif title.lower() in self.rules_fixartists:
|
||||
allartists = self.rules_fixartists[title.lower()]
|
||||
allartists = allartists.split("␟")
|
||||
if len(set(a.lower() for a in allartists) & set(a.lower() for a in artists)) > 0:
|
||||
artists = allartists
|
||||
artists = list(set(artists))
|
||||
artists.sort()
|
||||
|
||||
|
@ -120,7 +137,14 @@ class CleanerAgent:
|
|||
t = re.sub(r" \(originally by .*?\)","",t)
|
||||
t = re.sub(r" \(.*?Remaster.*?\)","",t)
|
||||
|
||||
return t.strip()
|
||||
for s in settings.get_settings("REMOVE_FROM_TITLE"):
|
||||
if s in t:
|
||||
t = t.replace(s,"")
|
||||
|
||||
t = t.strip()
|
||||
#for p in self.plugin_titleparsers:
|
||||
# t = p(t).strip()
|
||||
return t
|
||||
|
||||
def parseTitleForArtists(self,t):
|
||||
for d in self.delimiters_feat:
|
||||
|
@ -137,7 +161,10 @@ class CleanerAgent:
|
|||
artists += self.parseArtists(re.sub(r"(.*) " + d + " (.*).*",r"\2",t))
|
||||
return (title,artists)
|
||||
|
||||
return (t,[])
|
||||
artists = []
|
||||
for st in self.rules_artistintitle:
|
||||
if st in t.lower(): artists += self.rules_artistintitle[st].split("␟")
|
||||
return (t,artists)
|
||||
|
||||
|
||||
|
||||
|
@ -152,7 +179,7 @@ class CollectorAgent:
|
|||
# rules_include dict: credited artist -> all real artists
|
||||
|
||||
def updateRules(self):
|
||||
raw = tsv.parse_all("rules","string","string","string")
|
||||
raw = tsv.parse_all(datadir("rules"),"string","string","string")
|
||||
self.rules_countas = {b:c for [a,b,c] in raw if a=="countas"}
|
||||
self.rules_countas_id = {}
|
||||
self.rules_include = {} #Twice the memory, double the performance!
|
|
@ -1,11 +1,11 @@
|
|||
from doreah.logging import log
|
||||
import hashlib
|
||||
import random
|
||||
import database
|
||||
from . import database
|
||||
import datetime
|
||||
import itertools
|
||||
import sys
|
||||
from cleanup import CleanerAgent
|
||||
from .cleanup import CleanerAgent
|
||||
from bottle import response
|
||||
|
||||
## GNU-FM-compliant scrobbling
|
||||
|
@ -68,7 +68,7 @@ def handle(path,keys):
|
|||
|
||||
def scrobbletrack(artiststr,titlestr,timestamp):
|
||||
try:
|
||||
log("Incoming scrobble (compliant API): ARTISTS: " + artiststr + ", TRACK: " + titlestr,module="debug")
|
||||
log("Incoming scrobble (compliant API): ARTISTS: " + artiststr + ", TRACK: " + titlestr,module="debug")
|
||||
(artists,title) = cla.fullclean(artiststr,titlestr)
|
||||
database.createScrobble(artists,title,timestamp)
|
||||
database.sync()
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import shutil
|
||||
from distutils import dir_util
|
||||
import stat
|
||||
import pathlib
|
||||
import pkg_resources
|
||||
from doreah.control import mainfunction
|
||||
from doreah.io import col
|
||||
|
||||
from .globalconf import datadir
|
||||
from .backup import backup
|
||||
|
||||
|
||||
|
||||
|
||||
def copy_initial_local_files():
|
||||
folder = pkg_resources.resource_filename(__name__,"data_files")
|
||||
#shutil.copy(folder,DATA_DIR)
|
||||
dir_util.copy_tree(folder,datadir(),update=False)
|
||||
|
||||
|
||||
def setup():
|
||||
|
||||
copy_initial_local_files()
|
||||
|
||||
from doreah import settings
|
||||
|
||||
# EXTERNAL API KEYS
|
||||
apikeys = {
|
||||
"LASTFM_API_KEY":"Last.fm API Key",
|
||||
"FANARTTV_API_KEY":"Fanart.tv API Key",
|
||||
"SPOTIFY_API_ID":"Spotify Client ID",
|
||||
"SPOTIFY_API_SECRET":"Spotify Client Secret"
|
||||
}
|
||||
|
||||
print("Various external services can be used to display images. If not enough of them are set up, only local images will be used.")
|
||||
for k in apikeys:
|
||||
key = settings.get_settings(k)
|
||||
if key is None:
|
||||
print("\t" + "Currently not using a " + apikeys[k] + " for image display.")
|
||||
elif key == "ASK":
|
||||
print("\t" + "Please enter your " + apikeys[k] + ". If you do not want to use one at this moment, simply leave this empty and press Enter.")
|
||||
key = input()
|
||||
if key == "": key = None
|
||||
settings.update_settings(datadir("settings/settings.ini"),{k:key},create_new=True)
|
||||
else:
|
||||
print("\t" + apikeys[k] + " found.")
|
||||
|
||||
|
||||
# OWN API KEY
|
||||
if os.path.exists(datadir("clients/authenticated_machines.tsv")):
|
||||
pass
|
||||
else:
|
||||
print("Do you want to set up a key to enable scrobbling? Your scrobble extension needs that key so that only you can scrobble tracks to your database. [Y/n]")
|
||||
answer = input()
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
import random
|
||||
key = ""
|
||||
for i in range(64):
|
||||
key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
|
||||
print("Your API Key: " + col["yellow"](key))
|
||||
with open(datadir("clients/authenticated_machines.tsv"),"w") as keyfile:
|
||||
keyfile.write(key + "\t" + "Default Generated Key")
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
pass
|
||||
|
||||
|
||||
if settings.get_settings("NAME") is None:
|
||||
print("Please enter your name. This will be displayed e.g. when comparing your charts to another user. Leave this empty if you would not like to specify a name right now.")
|
||||
name = input()
|
||||
if name == "": name = "Generic Maloja User"
|
||||
settings.update_settings(datadir("settings/settings.ini"),{"NAME":name},create_new=True)
|
||||
|
||||
if settings.get_settings("SEND_STATS") is None:
|
||||
print("I would like to know how many people use Maloja. Would it be okay to send a daily ping to my server (this contains no data that isn't accessible via your web interface already)? [Y/n]")
|
||||
answer = input()
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
settings.update_settings(datadir("settings/settings.ini"),{"SEND_STATS":True,"PUBLIC_URL":None},create_new=True)
|
||||
else:
|
||||
settings.update_settings(datadir("settings/settings.ini"),{"SEND_STATS":False},create_new=True)
|
||||
|
||||
|
||||
def getInstance():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","Maloja"])
|
||||
pid = int(output)
|
||||
return pid
|
||||
except:
|
||||
return None
|
||||
|
||||
def getInstanceSupervisor():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","maloja_supervisor"])
|
||||
pid = int(output)
|
||||
return pid
|
||||
except:
|
||||
return None
|
||||
|
||||
def start():
|
||||
setup()
|
||||
try:
|
||||
p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
sp = subprocess.Popen(["python3","-m","maloja.supervisor"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
print(col["green"]("Maloja started!") + " PID: " + str(p.pid))
|
||||
|
||||
from doreah import settings
|
||||
port = settings.get_settings("WEB_PORT")
|
||||
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /setup to get started.")
|
||||
print("If you're installing this on your local machine, these links should get you there:")
|
||||
print("\t" + col["blue"]("http://localhost:" + str(port)))
|
||||
print("\t" + col["blue"]("http://localhost:" + str(port) + "/setup"))
|
||||
return True
|
||||
except:
|
||||
print("Error while starting Maloja.")
|
||||
return False
|
||||
|
||||
def restart():
|
||||
wasrunning = stop()
|
||||
start()
|
||||
return wasrunning
|
||||
|
||||
def stop():
|
||||
pid_sv = getInstanceSupervisor()
|
||||
if pid_sv is not None:
|
||||
os.kill(pid_sv,signal.SIGTERM)
|
||||
|
||||
pid = getInstance()
|
||||
if pid is None:
|
||||
print("Server is not running")
|
||||
return False
|
||||
else:
|
||||
os.kill(pid,signal.SIGTERM)
|
||||
print("Maloja stopped! PID: " + str(pid))
|
||||
return True
|
||||
|
||||
|
||||
def loadlastfm(filename):
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print("File could not be found.")
|
||||
return
|
||||
|
||||
if os.path.exists(datadir("scrobbles/lastfmimport.tsv")):
|
||||
print("Already imported Last.FM data. Overwrite? [y/N]")
|
||||
if input().lower() in ["y","yes","yea","1","positive","true"]:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
print("Please wait...")
|
||||
from .lastfmconverter import convert
|
||||
convert(filename,datadir("scrobbles/lastfmimport.tsv"))
|
||||
#os.system("python3 -m maloja.lastfmconverter " + filename + " " + datadir("scrobbles/lastfmimport.tsv"))
|
||||
print("Successfully imported your Last.FM scrobbles!")
|
||||
|
||||
def direct():
|
||||
from . import server
|
||||
|
||||
def backuphere():
|
||||
backup(folder=os.getcwd())
|
||||
|
||||
def update():
|
||||
os.system("pip3 install malojaserver --upgrade --no-cache-dir")
|
||||
restart()
|
||||
|
||||
def fixdb():
|
||||
from .fixexisting import fix
|
||||
fix()
|
||||
|
||||
@mainfunction({"l":"level"},shield=True)
|
||||
def main(action,*args,**kwargs):
|
||||
actions = {
|
||||
"start":restart,
|
||||
"restart":restart,
|
||||
"stop":stop,
|
||||
"import":loadlastfm,
|
||||
"debug":direct,
|
||||
"backup":backuphere,
|
||||
"update":update,
|
||||
"fix":fixdb
|
||||
}
|
||||
if action in actions: actions[action](*args,**kwargs)
|
||||
else: print("Valid commands: " + " ".join(a for a in actions))
|
||||
|
||||
return True
|
||||
|
||||
#if __name__ == "__main__":
|
||||
# main()
|
Can't render this file because it has a wrong number of fields in line 4.
|
Can't render this file because it has a wrong number of fields in line 5.
|
Can't render this file because it has a wrong number of fields in line 5.
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
@ -108,6 +108,8 @@ replacetitle 여자 대통령 Female President
|
|||
|
||||
# Mamamoo
|
||||
countas Hwasa Mamamoo
|
||||
countas Moonbyul Mamamoo
|
||||
replaceartist Moon Byul Moonbyul
|
||||
replaceartist Hwa Sa Hwasa
|
||||
replaceartist MAMAMOO Mamamoo
|
||||
replacetitle Egotistic(너나 해) Egotistic
|
||||
|
@ -154,3 +156,8 @@ replacetitle 벌써 12시 Gotta Go Gotta Go
|
|||
|
||||
# ITZY
|
||||
replacetitle 달라달라 (DALLA DALLA) Dalla Dalla
|
||||
|
||||
|
||||
# Popular Remixes
|
||||
artistintitle Areia Remix Areia
|
||||
artistintitle Areia Kpop Areia
|
Can't render this file because it has a wrong number of fields in line 5.
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
@ -0,0 +1,29 @@
|
|||
# NAME: Mass Effect Soundtrack
|
||||
# DESC: Sets correct artists for the Mass Effect soundtracks
|
||||
|
||||
# 1
|
||||
fixartists Jack Wall␟Sam Hulick Mass Effect Theme
|
||||
fixartists Richard Jacques␟Jack Wall␟Sam Hulick Spectre Induction
|
||||
fixartists Richard Jacques␟Jack Wall␟Sam Hulick The Citadel
|
||||
fixartists Richard Jacques␟Jack Wall The Thorian
|
||||
fixartists Richard Jacques␟Jack Wall␟Sam Hulick The Alien Queen
|
||||
fixartists Jack Wall␟Sam Hulick Breeding Ground
|
||||
fixartists Jack Wall␟Sam Hulick In Pursuit of Saren
|
||||
fixartists David Kates␟Jack Wall␟Sam Hulick Infusion
|
||||
fixartists David Kates␟Jack Wall␟Sam Hulick Final Assault
|
||||
|
||||
# 2
|
||||
fixartists Jack Wall␟David Kates Thane
|
||||
fixartists Jack Wall␟Sam Hulick The Normandy Attacked
|
||||
fixartists Jack Wall␟Brian DiDomenico The Collector Base
|
||||
fixartists Jack Wall␟Sam Hulick New Worlds
|
||||
|
||||
# 3
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco The Ardat-Yakshi
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco Rannoch
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco I'm Sorry
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco The Scientists
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco Aralakh Company
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco Prothean Beacon
|
||||
fixartists Sascha Dikiciyan␟Cris Velasco Reaper Chase
|
||||
fixartists Clint Mansell␟Sam Hulick An End, Once and For All
|
Can't render this file because it has a wrong number of fields in line 5.
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
@ -20,6 +20,12 @@ The first column defines the type of the rule:
|
|||
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 ␟
|
||||
fixartists Similar as above, but simply specifies that if any of the given artists is present, all (and no others) should be present
|
||||
Second column is correct artists
|
||||
Third column is the song title
|
||||
artistintitle Defines title strings that imply the presence of another artist.
|
||||
Second column is the string
|
||||
Third column is the artist or artists
|
||||
|
||||
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.
|
||||
|
@ -35,3 +41,4 @@ replaceartist Dal Shabet Dal★Shabet
|
|||
replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 # one artist is replaced by two artists
|
||||
countas Trouble Maker HyunA
|
||||
addartists HyunA Change Jun Hyung
|
||||
artistintitle Areia Remix Areia
|
|
@ -16,6 +16,8 @@ SPOTIFY_API_ID = "ASK"
|
|||
SPOTIFY_API_SECRET = "ASK"
|
||||
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
|
||||
THUMBOR_SERVER = None
|
||||
THUMBOR_SECRET = ""
|
||||
|
||||
# Can be 'YouTube', 'YouTube Music', 'Google Play Music', 'Spotify', 'Tidal', 'SoundCloud', 'Deezer', 'Amazon Music'
|
||||
# Omit or set to none to disable
|
||||
|
@ -25,6 +27,8 @@ TRACK_SEARCH_PROVIDER = None
|
|||
|
||||
DB_CACHE_SIZE = 8192 # how many MB on disk each database cache should have available.
|
||||
INVALID_ARTISTS = ["[Unknown Artist]","Unknown Artist","Spotify"]
|
||||
REMOVE_FROM_TITLE = ["(Original Mix)","(Radio Edit)","(Album Version)","(Explicit Version)"]
|
||||
USE_PARSE_PLUGINS = no
|
||||
|
||||
[Local Images]
|
||||
|
||||
|
@ -51,9 +55,11 @@ SCROBBLES_GOLD = 250
|
|||
SCROBBLES_PLATINUM = 500
|
||||
SCROBBLES_DIAMOND = 1000
|
||||
# name for comparisons
|
||||
NAME = "Generic Maloja User"
|
||||
NAME = None
|
||||
|
||||
[Misc]
|
||||
|
||||
EXPERIMENTAL_FEATURES = no
|
||||
USE_PYHP = no
|
||||
USE_PYHP = no #not recommended at the moment
|
||||
FEDERATION = yes #does nothing yet
|
||||
UPDATE_AFTER_CRASH = no #update when server is automatically restarted
|
|
@ -1,12 +1,14 @@
|
|||
# server
|
||||
from bottle import request, response, FormsDict
|
||||
# rest of the project
|
||||
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
|
||||
from external import proxy_scrobble
|
||||
from .cleanup import CleanerAgent, CollectorAgent
|
||||
from . import utilities
|
||||
from .malojatime import register_scrobbletime, time_stamps, ranges
|
||||
from .urihandler import uri_to_internal, internal_to_uri, compose_querystring
|
||||
from . import compliant_api
|
||||
from .external import proxy_scrobble
|
||||
from .__init__ import version
|
||||
from .globalconf import datadir
|
||||
# doreah toolkit
|
||||
from doreah.logging import log
|
||||
from doreah import tsv
|
||||
|
@ -26,6 +28,7 @@ import sys
|
|||
import unicodedata
|
||||
from collections import namedtuple
|
||||
from threading import Lock
|
||||
import yaml
|
||||
# url handling
|
||||
from importlib.machinery import SourceFileLoader
|
||||
import urllib
|
||||
|
@ -46,10 +49,10 @@ Scrobble = namedtuple("Scrobble",["track","timestamp","saved"])
|
|||
SCROBBLESDICT = {} # timestamps to scrobble mapping
|
||||
STAMPS = [] # sorted
|
||||
#STAMPS_SET = set() # as set for easier check if exists # we use the scrobbles dict for that now
|
||||
TRACKS_LOWER = []
|
||||
ARTISTS_LOWER = []
|
||||
ARTIST_SET = set()
|
||||
TRACK_SET = set()
|
||||
TRACKS_NORMALIZED = []
|
||||
ARTISTS_NORMALIZED = []
|
||||
ARTISTS_NORMALIZED_SET = set()
|
||||
TRACKS_NORMALIZED_SET = set()
|
||||
|
||||
MEDALS = {} #literally only changes once per year, no need to calculate that on the fly
|
||||
MEDALS_TRACKS = {}
|
||||
|
@ -65,14 +68,26 @@ lastsync = 0
|
|||
# rulestate that the entire current database was built with, or False if the database was built from inconsistent scrobble files
|
||||
db_rulestate = False
|
||||
|
||||
try:
|
||||
with open(datadir("known_servers.yml"),"r") as f:
|
||||
KNOWN_SERVERS = set(yaml.safe_load(f))
|
||||
except:
|
||||
KNOWN_SERVERS = set()
|
||||
|
||||
|
||||
def add_known_server(url):
|
||||
KNOWN_SERVERS.add(url)
|
||||
with open(datadir("known_servers.yml"),"w") as f:
|
||||
f.write(yaml.dump(list(KNOWN_SERVERS)))
|
||||
|
||||
|
||||
|
||||
### symmetric keys are fine for now since we hopefully use HTTPS
|
||||
def loadAPIkeys():
|
||||
global clients
|
||||
tsv.create("clients/authenticated_machines.tsv")
|
||||
tsv.create(datadir("clients/authenticated_machines.tsv"))
|
||||
#createTSV("clients/authenticated_machines.tsv")
|
||||
clients = tsv.parse("clients/authenticated_machines.tsv","string","string")
|
||||
clients = tsv.parse(datadir("clients/authenticated_machines.tsv"),"string","string")
|
||||
#clients = parseTSV("clients/authenticated_machines.tsv","string","string")
|
||||
log("Authenticated Machines: " + ", ".join([m[1] for m in clients]))
|
||||
|
||||
|
@ -158,16 +173,16 @@ def readScrobble(artists,title,time):
|
|||
def getArtistID(name):
|
||||
|
||||
obj = name
|
||||
objlower = name.lower().replace("'","")
|
||||
obj_normalized = normalize_name(name)
|
||||
|
||||
if objlower in ARTIST_SET:
|
||||
return ARTISTS_LOWER.index(objlower)
|
||||
if obj_normalized in ARTISTS_NORMALIZED_SET:
|
||||
return ARTISTS_NORMALIZED.index(obj_normalized)
|
||||
|
||||
else:
|
||||
i = len(ARTISTS)
|
||||
ARTISTS.append(obj)
|
||||
ARTIST_SET.add(objlower)
|
||||
ARTISTS_LOWER.append(objlower)
|
||||
ARTISTS_NORMALIZED_SET.add(obj_normalized)
|
||||
ARTISTS_NORMALIZED.append(obj_normalized)
|
||||
|
||||
# with a new artist added, we might also get new artists that they are credited as
|
||||
cr = coa.getCredited(name)
|
||||
|
@ -182,20 +197,24 @@ def getTrackID(artists,title):
|
|||
for a in artists:
|
||||
artistset.add(getArtistID(name=a))
|
||||
obj = Track(artists=frozenset(artistset),title=title)
|
||||
objlower = Track(artists=frozenset(artistset),title=title.lower().replace("'",""))
|
||||
obj_normalized = Track(artists=frozenset(artistset),title=normalize_name(title))
|
||||
|
||||
if objlower in TRACK_SET:
|
||||
return TRACKS_LOWER.index(objlower)
|
||||
if obj_normalized in TRACKS_NORMALIZED_SET:
|
||||
return TRACKS_NORMALIZED.index(obj_normalized)
|
||||
else:
|
||||
i = len(TRACKS)
|
||||
TRACKS.append(obj)
|
||||
TRACK_SET.add(objlower)
|
||||
TRACKS_LOWER.append(objlower)
|
||||
TRACKS_NORMALIZED_SET.add(obj_normalized)
|
||||
TRACKS_NORMALIZED.append(obj_normalized)
|
||||
return i
|
||||
|
||||
import unicodedata
|
||||
|
||||
|
||||
|
||||
# function to turn the name into a representation that can be easily compared, ignoring minor differences
|
||||
remove_symbols = ["'","`","’"]
|
||||
def normalize_name(name):
|
||||
return "".join(char for char in unicodedata.normalize('NFD',name.lower())
|
||||
if char not in remove_symbols and unicodedata.category(char) != 'Mn')
|
||||
|
||||
|
||||
|
||||
|
@ -232,14 +251,15 @@ def test_server(key=None):
|
|||
|
||||
@dbserver.get("serverinfo")
|
||||
def server_info():
|
||||
import info
|
||||
|
||||
|
||||
response.set_header("Access-Control-Allow-Origin","*")
|
||||
response.set_header("Content-Type","application/json")
|
||||
|
||||
return {
|
||||
"name":settings.get_settings("NAME"),
|
||||
"version":info.version
|
||||
"version":version,
|
||||
"versionstring":".".join(str(n) for n in version)
|
||||
}
|
||||
|
||||
## 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
|
||||
|
@ -263,6 +283,10 @@ def get_scrobbles(**keys):
|
|||
# info for comparison
|
||||
@dbserver.get("info")
|
||||
def info_external(**keys):
|
||||
|
||||
response.set_header("Access-Control-Allow-Origin","*")
|
||||
response.set_header("Content-Type","application/json")
|
||||
|
||||
result = info()
|
||||
return result
|
||||
|
||||
|
@ -274,7 +298,9 @@ def info():
|
|||
"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}
|
||||
for chartentry in get_charts_artists() if chartentry["scrobbles"]/totalscrobbles >= 0
|
||||
},
|
||||
"known_servers":list(KNOWN_SERVERS)
|
||||
}
|
||||
|
||||
|
||||
|
@ -826,10 +852,10 @@ def import_rulemodule(**keys):
|
|||
|
||||
if remove:
|
||||
log("Deactivating predefined rulefile " + filename)
|
||||
os.remove("rules/" + filename + ".tsv")
|
||||
os.remove(datadir("rules/" + filename + ".tsv"))
|
||||
else:
|
||||
log("Importing predefined rulefile " + filename)
|
||||
os.symlink("predefined/" + filename + ".tsv","rules/" + filename + ".tsv")
|
||||
os.symlink(datadir("rules/predefined/" + filename + ".tsv"),datadir("rules/" + filename + ".tsv"))
|
||||
|
||||
|
||||
|
||||
|
@ -841,7 +867,8 @@ def rebuild(**keys):
|
|||
global db_rulestate
|
||||
db_rulestate = False
|
||||
sync()
|
||||
os.system("python3 fixexisting.py")
|
||||
from .fixexisting import fix
|
||||
fix()
|
||||
global cla, coa
|
||||
cla = CleanerAgent()
|
||||
coa = CollectorAgent()
|
||||
|
@ -929,7 +956,7 @@ def build_db():
|
|||
|
||||
|
||||
# parse files
|
||||
db = tsv.parse_all("scrobbles","int","string","string",comments=False)
|
||||
db = tsv.parse_all(datadir("scrobbles"),"int","string","string",comments=False)
|
||||
#db = parseAllTSV("scrobbles","int","string","string",escape=False)
|
||||
for sc in db:
|
||||
artists = sc[1].split("␟")
|
||||
|
@ -960,9 +987,10 @@ def build_db():
|
|||
#start regular tasks
|
||||
utilities.update_medals()
|
||||
utilities.update_weekly()
|
||||
utilities.send_stats()
|
||||
|
||||
global db_rulestate
|
||||
db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums)
|
||||
db_rulestate = utilities.consistentRulestate(datadir("scrobbles"),cla.checksums)
|
||||
|
||||
log("Database fully built!")
|
||||
|
||||
|
@ -996,9 +1024,9 @@ def sync():
|
|||
#log("Sorted into months",module="debug")
|
||||
|
||||
for e in entries:
|
||||
tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False)
|
||||
tsv.add_entries(datadir("scrobbles/" + e + ".tsv"),entries[e],comments=False)
|
||||
#addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False)
|
||||
utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
|
||||
utilities.combineChecksums(datadir("scrobbles/" + e + ".tsv"),cla.checksums)
|
||||
|
||||
#log("Written files",module="debug")
|
||||
|
||||
|
@ -1023,7 +1051,7 @@ import copy
|
|||
|
||||
cache_query = {}
|
||||
if doreah.version >= (0,7,1) and settings.get_settings("EXPERIMENTAL_FEATURES"):
|
||||
cache_query_permanent = DiskDict(name="dbquery",folder="cache",maxmemory=1024*1024*500,maxstorage=1024*1024*settings.get_settings("DB_CACHE_SIZE"))
|
||||
cache_query_permanent = DiskDict(name="dbquery",folder=datadir("cache"),maxmemory=1024*1024*500,maxstorage=1024*1024*settings.get_settings("DB_CACHE_SIZE"))
|
||||
else:
|
||||
cache_query_permanent = Cache(maxmemory=1024*1024*500)
|
||||
cacheday = (0,0,0)
|
|
@ -63,7 +63,6 @@ if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_
|
|||
def api_request_artist(artist):
|
||||
for api in apis_artists:
|
||||
if True:
|
||||
log("API: " + api["name"] + "; Image request: " + artist,module="external")
|
||||
try:
|
||||
artiststring = urllib.parse.quote(artist)
|
||||
var = artiststring
|
||||
|
@ -85,7 +84,9 @@ def api_request_artist(artist):
|
|||
for node in step[1]:
|
||||
var = var[node]
|
||||
assert isinstance(var,str) and var != ""
|
||||
except:
|
||||
except Exception as e:
|
||||
log("Error while getting artist image from " + api["name"],module="external")
|
||||
log(str(e),module="external")
|
||||
continue
|
||||
|
||||
return var
|
||||
|
@ -97,7 +98,6 @@ def api_request_track(track):
|
|||
artists, title = track
|
||||
for api in apis_tracks:
|
||||
if True:
|
||||
log("API: " + api["name"] + "; Image request: " + "/".join(artists) + " - " + title,module="external")
|
||||
try:
|
||||
artiststring = urllib.parse.quote(", ".join(artists))
|
||||
titlestring = urllib.parse.quote(title)
|
|
@ -0,0 +1,64 @@
|
|||
import os
|
||||
from .globalconf import datadir
|
||||
import re
|
||||
from .cleanup import CleanerAgent
|
||||
from doreah.logging import log
|
||||
import difflib
|
||||
import datetime
|
||||
from .backup import backup
|
||||
|
||||
wendigo = CleanerAgent()
|
||||
|
||||
exp = r"([0-9]*)(\t+)([^\t]+?)(\t+)([^\t]+)(\t*)([^\t]*)\n"
|
||||
|
||||
|
||||
|
||||
def fix():
|
||||
|
||||
backup(level="minimal",folder=datadir("backups"))
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
nowstr = now.strftime("%Y_%m_%d_%H_%M_%S")
|
||||
datestr = now.strftime("%Y/%m/%d")
|
||||
timestr = now.strftime("%H:%M:%S")
|
||||
|
||||
with open(datadir("logs","dbfix",nowstr + ".log"),"a") as logfile:
|
||||
|
||||
logfile.write("Database fix initiated on " + datestr + " " + timestr + " UTC")
|
||||
logfile.write("\n\n")
|
||||
|
||||
for filename in os.listdir(datadir("scrobbles")):
|
||||
if filename.endswith(".tsv"):
|
||||
filename_new = filename + "_new"
|
||||
|
||||
with open(datadir("scrobbles",filename_new),"w") as newfile:
|
||||
with open(datadir("scrobbles",filename),"r") as oldfile:
|
||||
|
||||
for l in oldfile:
|
||||
|
||||
a,t = re.sub(exp,r"\3",l), re.sub(exp,r"\5",l)
|
||||
r1,r2,r3 = re.sub(exp,r"\1\2",l),re.sub(exp,r"\4",l),re.sub(exp,r"\6\7",l)
|
||||
|
||||
a = a.replace("␟",";")
|
||||
|
||||
(al,t) = wendigo.fullclean(a,t)
|
||||
a = "␟".join(al)
|
||||
newfile.write(r1 + a + r2 + t + r3 + "\n")
|
||||
|
||||
|
||||
#os.system("diff " + "scrobbles/" + fn + "_new" + " " + "scrobbles/" + fn)
|
||||
with open(datadir("scrobbles",filename_new),"r") as newfile:
|
||||
with open(datadir("scrobbles",filename),"r") as oldfile:
|
||||
|
||||
diff = difflib.unified_diff(oldfile.read().split("\n"),newfile.read().split("\n"),lineterm="")
|
||||
diff = list(diff)[2:]
|
||||
#log("Diff for scrobbles/" + filename + "".join("\n\t" + d for d in diff),module="fixer")
|
||||
output = "Diff for scrobbles/" + filename + "".join("\n\t" + d for d in diff)
|
||||
print(output)
|
||||
logfile.write(output)
|
||||
logfile.write("\n")
|
||||
|
||||
os.rename(datadir("scrobbles",filename_new),datadir("scrobbles",filename))
|
||||
|
||||
with open(datadir("scrobbles",filename + ".rulestate"),"w") as checkfile:
|
||||
checkfile.write(wendigo.checksums)
|
|
@ -0,0 +1,62 @@
|
|||
import os
|
||||
|
||||
|
||||
# data folder
|
||||
# must be determined first because getting settings relies on it
|
||||
|
||||
try:
|
||||
DATA_DIR = os.environ["XDG_DATA_HOME"].split(":")[0]
|
||||
assert os.path.exists(DATA_DIR)
|
||||
except:
|
||||
DATA_DIR = os.path.join(os.environ["HOME"],".local/share/")
|
||||
|
||||
DATA_DIR = os.path.join(DATA_DIR,"maloja")
|
||||
os.makedirs(DATA_DIR,exist_ok=True)
|
||||
|
||||
def datadir(*args):
|
||||
return os.path.join(DATA_DIR,*args)
|
||||
|
||||
|
||||
|
||||
### DOREAH CONFIGURATION
|
||||
|
||||
from doreah import config
|
||||
|
||||
config(
|
||||
pyhp={
|
||||
"version": 2
|
||||
},
|
||||
logging={
|
||||
"logfolder": datadir("logs")
|
||||
},
|
||||
settings={
|
||||
"files":[
|
||||
datadir("settings/default.ini"),
|
||||
datadir("settings/settings.ini")
|
||||
]
|
||||
},
|
||||
caching={
|
||||
"folder": datadir("cache")
|
||||
},
|
||||
regular={
|
||||
"autostart": False
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
from doreah.settings import get_settings
|
||||
|
||||
# thumbor
|
||||
|
||||
THUMBOR_SERVER, THUMBOR_SECRET = get_settings("THUMBOR_SERVER","THUMBOR_SECRET")
|
||||
try:
|
||||
USE_THUMBOR = THUMBOR_SERVER is not None and THUMBOR_SECRET is not None
|
||||
if USE_THUMBOR:
|
||||
from libthumbor import CryptoURL
|
||||
THUMBOR_GENERATOR = CryptoURL(key=THUMBOR_SECRET)
|
||||
OWNURL = get_settings("PUBLIC_URL")
|
||||
assert OWNURL is not None
|
||||
except:
|
||||
USE_THUMBOR = False
|
||||
log("Thumbor could not be initialized. Is libthumbor installed?")
|
|
@ -1,7 +1,7 @@
|
|||
import urllib
|
||||
from bottle import FormsDict
|
||||
import datetime
|
||||
from urihandler import compose_querystring
|
||||
from .urihandler import compose_querystring
|
||||
import urllib.parse
|
||||
from doreah.settings import get_settings
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
from htmlgenerators import *
|
||||
import database
|
||||
from utilities import getArtistImage, getTrackImage
|
||||
from malojatime import *
|
||||
from urihandler import compose_querystring, internal_to_uri, uri_to_internal
|
||||
from .htmlgenerators import *
|
||||
from . import database
|
||||
from .utilities import getArtistImage, getTrackImage
|
||||
from .malojatime import *
|
||||
from .urihandler import compose_querystring, internal_to_uri, uri_to_internal
|
||||
import urllib
|
||||
import datetime
|
||||
import math
|
||||
|
@ -568,7 +568,7 @@ def module_paginate(page,pages,perpage,**keys):
|
|||
# THIS FUNCTION USES THE ORIGINAL URI KEYS!!!
|
||||
def module_filterselection(keys,time=True,delimit=False):
|
||||
|
||||
from malojatime import today, thisweek, thismonth, thisyear, alltime
|
||||
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
|
|
@ -0,0 +1,69 @@
|
|||
import os, datetime, re
|
||||
from .cleanup import *
|
||||
from .utilities import *
|
||||
|
||||
|
||||
|
||||
|
||||
c = CleanerAgent()
|
||||
|
||||
|
||||
|
||||
def convert(input,output):
|
||||
|
||||
log = open(input,"r")
|
||||
outputlog = open(output,"w")
|
||||
checksumfile = open(output + ".rulestate","w") #this file stores an identifier for all rules that were in place when the corresponding file was created
|
||||
|
||||
stamps = [99999999999999]
|
||||
|
||||
for l in log:
|
||||
l = l.replace("\n","")
|
||||
data = l.split(",")
|
||||
|
||||
artist = data[0]
|
||||
album = data[1]
|
||||
title = data[2]
|
||||
time = data[3]
|
||||
|
||||
|
||||
(artists,title) = c.fullclean(artist,title)
|
||||
|
||||
artistsstr = "␟".join(artists)
|
||||
|
||||
|
||||
timeparts = time.split(" ")
|
||||
(h,m) = timeparts[3].split(":")
|
||||
|
||||
months = {"Jan":1,"Feb":2,"Mar":3,"Apr":4,"May":5,"Jun":6,"Jul":7,"Aug":8,"Sep":9,"Oct":10,"Nov":11,"Dec":12}
|
||||
|
||||
timestamp = int(datetime.datetime(int(timeparts[2]),months[timeparts[1]],int(timeparts[0]),int(h),int(m)).timestamp())
|
||||
|
||||
|
||||
## We prevent double timestamps in the database creation, so we technically don't need them in the files
|
||||
## however since the conversion from lastfm to maloja is a one-time thing, we should take any effort to make the file as good as possible
|
||||
if (timestamp < stamps[-1]):
|
||||
pass
|
||||
elif (timestamp == stamps[-1]):
|
||||
timestamp -= 1
|
||||
else:
|
||||
while(timestamp in stamps):
|
||||
timestamp -= 1
|
||||
|
||||
if (timestamp < stamps[-1]):
|
||||
stamps.append(timestamp)
|
||||
else:
|
||||
stamps.insert(0,timestamp)
|
||||
|
||||
|
||||
entry = "\t".join([str(timestamp),artistsstr,title,album])
|
||||
entry = entry.replace("#",r"\num")
|
||||
|
||||
outputlog.write(entry)
|
||||
outputlog.write("\n")
|
||||
|
||||
checksumfile.write(c.checksums)
|
||||
|
||||
log.close()
|
||||
outputlog.close()
|
||||
checksumfile.close()
|
|
@ -1,36 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
from .globalconf import datadir, DATA_DIR
|
||||
|
||||
|
||||
# server stuff
|
||||
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template, HTTPResponse, BaseRequest
|
||||
import waitress
|
||||
# monkey patching
|
||||
import monkey
|
||||
from . import monkey
|
||||
# rest of the project
|
||||
import database
|
||||
import htmlmodules
|
||||
import htmlgenerators
|
||||
import malojatime
|
||||
import utilities
|
||||
from utilities import resolveImage
|
||||
from urihandler import uri_to_internal, remove_identical
|
||||
import urihandler
|
||||
import info
|
||||
from . import database
|
||||
from . import htmlmodules
|
||||
from . import htmlgenerators
|
||||
from . import malojatime
|
||||
from . import utilities
|
||||
from .utilities import resolveImage
|
||||
from .urihandler import uri_to_internal, remove_identical
|
||||
from . import urihandler
|
||||
from . import globalconf
|
||||
# doreah toolkit
|
||||
from doreah import settings
|
||||
from doreah.logging import log
|
||||
from doreah.timing import Clock
|
||||
from doreah.pyhp import file as pyhpfile
|
||||
# technical
|
||||
from importlib.machinery import SourceFileLoader
|
||||
#from importlib.machinery import SourceFileLoader
|
||||
import importlib
|
||||
import _thread
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import setproctitle
|
||||
import pkg_resources
|
||||
# url handling
|
||||
import urllib
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#settings.config(files=["settings/default.ini","settings/settings.ini"])
|
||||
#settings.update("settings/default.ini","settings/settings.ini")
|
||||
MAIN_PORT = settings.get_settings("WEB_PORT")
|
||||
|
@ -38,18 +47,30 @@ HOST = settings.get_settings("HOST")
|
|||
THREADS = 12
|
||||
BaseRequest.MEMFILE_MAX = 15 * 1024 * 1024
|
||||
|
||||
WEBFOLDER = pkg_resources.resource_filename(__name__,"web")
|
||||
STATICFOLDER = pkg_resources.resource_filename(__name__,"static")
|
||||
DATAFOLDER = DATA_DIR
|
||||
|
||||
webserver = Bottle()
|
||||
|
||||
pthjoin = os.path.join
|
||||
|
||||
import lesscpy
|
||||
css = ""
|
||||
for f in os.listdir("website/less"):
|
||||
css += lesscpy.compile("website/less/" + f)
|
||||
def generate_css():
|
||||
import lesscpy
|
||||
from io import StringIO
|
||||
less = ""
|
||||
for f in os.listdir(pthjoin(STATICFOLDER,"less")):
|
||||
with open(pthjoin(STATICFOLDER,"less",f),"r") as lessf:
|
||||
less += lessf.read()
|
||||
|
||||
os.makedirs("website/css",exist_ok=True)
|
||||
with open("website/css/style.css","w") as f:
|
||||
f.write(css)
|
||||
css = lesscpy.compile(StringIO(less),minify=True)
|
||||
return css
|
||||
|
||||
css = generate_css()
|
||||
|
||||
#os.makedirs("web/css",exist_ok=True)
|
||||
#with open("web/css/style.css","w") as f:
|
||||
# f.write(css)
|
||||
|
||||
|
||||
@webserver.route("")
|
||||
|
@ -67,23 +88,11 @@ def mainpage():
|
|||
@webserver.error(505)
|
||||
def customerror(error):
|
||||
code = int(str(error).split(",")[0][1:])
|
||||
log("HTTP Error: " + str(code),module="error")
|
||||
|
||||
if os.path.exists("website/errors/" + str(code) + ".html"):
|
||||
return static_file("website/errors/" + str(code) + ".html",root="")
|
||||
if os.path.exists(pthjoin(WEBFOLDER,"errors",str(code) + ".pyhp")):
|
||||
return pyhpfile(pthjoin(WEBFOLDER,"errors",str(code) + ".pyhp"),{"errorcode":code})
|
||||
else:
|
||||
with open("website/errors/generic.html") as htmlfile:
|
||||
html = htmlfile.read()
|
||||
|
||||
# apply global substitutions
|
||||
with open("website/common/footer.html") as footerfile:
|
||||
footerhtml = footerfile.read()
|
||||
with open("website/common/header.html") as headerfile:
|
||||
headerhtml = headerfile.read()
|
||||
html = html.replace("</body>",footerhtml + "</body>").replace("</head>",headerhtml + "</head>")
|
||||
|
||||
html = html.replace("ERROR_CODE",str(code))
|
||||
return html
|
||||
return pyhpfile(pthjoin(WEBFOLDER,"errors","generic.pyhp"),{"errorcode":code})
|
||||
|
||||
|
||||
|
||||
|
@ -111,49 +120,63 @@ def dynamic_image():
|
|||
@webserver.route("/images/<pth:re:.*\\.png>")
|
||||
@webserver.route("/images/<pth:re:.*\\.gif>")
|
||||
def static_image(pth):
|
||||
if globalconf.USE_THUMBOR:
|
||||
return static_file(pthjoin("images",pth),root=DATAFOLDER)
|
||||
|
||||
type = pth.split(".")[-1]
|
||||
small_pth = pth + "-small"
|
||||
if os.path.exists("images/" + small_pth):
|
||||
response = static_file("images/" + small_pth,root="")
|
||||
if os.path.exists(datadir("images",small_pth)):
|
||||
response = static_file(pthjoin("images",small_pth),root=DATAFOLDER)
|
||||
else:
|
||||
try:
|
||||
from wand.image import Image
|
||||
img = Image(filename="images/" + pth)
|
||||
img = Image(filename=datadir("images",pth))
|
||||
x,y = img.size[0], img.size[1]
|
||||
smaller = min(x,y)
|
||||
if smaller > 300:
|
||||
ratio = 300/smaller
|
||||
img.resize(int(ratio*x),int(ratio*y))
|
||||
img.save(filename="images/" + small_pth)
|
||||
response = static_file("images/" + small_pth,root="")
|
||||
img.save(filename=datadir("images",small_pth))
|
||||
response = static_file(pthjoin("images",small_pth),root=DATAFOLDER)
|
||||
else:
|
||||
response = static_file("images/" + pth,root="")
|
||||
response = static_file(pthjoin("images",pth),root=DATAFOLDER)
|
||||
except:
|
||||
response = static_file("images/" + pth,root="")
|
||||
response = static_file(pthjoin("images",pth),root=DATAFOLDER)
|
||||
|
||||
#response = static_file("images/" + pth,root="")
|
||||
response.set_header("Cache-Control", "public, max-age=86400")
|
||||
response.set_header("Content-Type", "image/" + type)
|
||||
return response
|
||||
|
||||
#@webserver.route("/<name:re:.*\\.html>")
|
||||
@webserver.route("/<name:re:.*\\.js>")
|
||||
@webserver.route("/<name:re:.*\\.css>")
|
||||
@webserver.route("/<name:re:.*\\.less>")
|
||||
@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="")
|
||||
|
||||
@webserver.route("/style.css")
|
||||
def get_css():
|
||||
response.content_type = 'text/css'
|
||||
return css
|
||||
|
||||
|
||||
@webserver.route("/<name>.<ext>")
|
||||
def static(name,ext):
|
||||
assert ext in ["txt","ico","jpeg","jpg","png","less","js"]
|
||||
response = static_file(ext + "/" + name + "." + ext,root=STATICFOLDER)
|
||||
response.set_header("Cache-Control", "public, max-age=3600")
|
||||
return response
|
||||
|
||||
@webserver.route("/media/<name>.<ext>")
|
||||
def static(name,ext):
|
||||
assert ext in ["ico","jpeg","jpg","png"]
|
||||
response = static_file(ext + "/" + name + "." + ext,root=STATICFOLDER)
|
||||
response.set_header("Cache-Control", "public, max-age=3600")
|
||||
return response
|
||||
|
||||
|
||||
@webserver.route("/<name>")
|
||||
def static_html(name):
|
||||
linkheaders = ["</css/style.css>; rel=preload; as=style"]
|
||||
linkheaders = ["</style.css>; rel=preload; as=style"]
|
||||
keys = remove_identical(FormsDict.decode(request.query))
|
||||
|
||||
pyhp_file = os.path.exists("website/" + name + ".pyhp")
|
||||
html_file = os.path.exists("website/" + name + ".html")
|
||||
pyhp_file = os.path.exists(pthjoin(WEBFOLDER,name + ".pyhp"))
|
||||
html_file = os.path.exists(pthjoin(WEBFOLDER,name + ".html"))
|
||||
pyhp_pref = settings.get_settings("USE_PYHP")
|
||||
|
||||
adminmode = request.cookies.get("adminmode") == "true" and database.checkAPIkey(request.cookies.get("apikey")) is not False
|
||||
|
@ -163,49 +186,51 @@ def static_html(name):
|
|||
|
||||
# if a pyhp file exists, use this
|
||||
if (pyhp_file and pyhp_pref) or (pyhp_file and not html_file):
|
||||
from doreah.pyhp import file
|
||||
environ = {} #things we expose to the pyhp pages
|
||||
|
||||
environ["adminmode"] = adminmode
|
||||
if adminmode: environ["apikey"] = request.cookies.get("apikey")
|
||||
|
||||
# maloja
|
||||
environ["db"] = database
|
||||
environ["htmlmodules"] = htmlmodules
|
||||
environ["htmlgenerators"] = htmlgenerators
|
||||
environ["malojatime"] = malojatime
|
||||
environ["utilities"] = utilities
|
||||
environ["urihandler"] = urihandler
|
||||
environ["info"] = info
|
||||
# external
|
||||
environ["urllib"] = urllib
|
||||
#things we expose to the pyhp pages
|
||||
environ = {
|
||||
"adminmode":adminmode,
|
||||
"apikey":request.cookies.get("apikey") if adminmode else None,
|
||||
# maloja
|
||||
"db": database,
|
||||
"htmlmodules": htmlmodules,
|
||||
"htmlgenerators": htmlgenerators,
|
||||
"malojatime": malojatime,
|
||||
"utilities": utilities,
|
||||
"urihandler": urihandler,
|
||||
"settings": settings.get_settings,
|
||||
# external
|
||||
"urllib": urllib
|
||||
}
|
||||
# request
|
||||
environ["filterkeys"], environ["limitkeys"], environ["delimitkeys"], environ["amountkeys"] = uri_to_internal(keys)
|
||||
environ["_urikeys"] = keys #temporary!
|
||||
|
||||
#response.set_header("Content-Type","application/xhtml+xml")
|
||||
res = file("website/" + name + ".pyhp",environ)
|
||||
log("Generated page " + name + " in " + str(clock.stop()) + "s (PYHP)",module="debug")
|
||||
res = pyhpfile(pthjoin(WEBFOLDER,name + ".pyhp"),environ)
|
||||
log("Generated page {name} in {time}s (PYHP)".format(name=name,time=clock.stop()),module="debug")
|
||||
return res
|
||||
|
||||
# if not, use the old way
|
||||
else:
|
||||
|
||||
with open("website/" + name + ".html") as htmlfile:
|
||||
with open(pthjoin(WEBFOLDER,name + ".html")) as htmlfile:
|
||||
html = htmlfile.read()
|
||||
|
||||
# apply global substitutions
|
||||
with open("website/common/footer.html") as footerfile:
|
||||
with open(pthjoin(WEBFOLDER,"common/footer.html")) as footerfile:
|
||||
footerhtml = footerfile.read()
|
||||
with open("website/common/header.html") as headerfile:
|
||||
with open(pthjoin(WEBFOLDER,"common/header.html")) as headerfile:
|
||||
headerhtml = headerfile.read()
|
||||
html = html.replace("</body>",footerhtml + "</body>").replace("</head>",headerhtml + "</head>")
|
||||
|
||||
|
||||
# If a python file exists, it provides the replacement dict for the html file
|
||||
if os.path.exists("website/" + name + ".py"):
|
||||
#txt_keys = SourceFileLoader(name,"website/" + name + ".py").load_module().replacedict(keys,DATABASE_PORT)
|
||||
if os.path.exists(pthjoin(WEBFOLDER,name + ".py")):
|
||||
#txt_keys = SourceFileLoader(name,"web/" + name + ".py").load_module().replacedict(keys,DATABASE_PORT)
|
||||
try:
|
||||
txt_keys,resources = SourceFileLoader(name,"website/" + name + ".py").load_module().instructions(keys)
|
||||
module = importlib.import_module(".web." + name,package="maloja")
|
||||
txt_keys,resources = module.instructions(keys)
|
||||
except Exception as e:
|
||||
log("Error in website generation: " + str(sys.exc_info()),module="error")
|
||||
raise
|
||||
|
@ -229,7 +254,7 @@ def static_html(name):
|
|||
response.set_header("Link",",".join(linkheaders))
|
||||
log("Generated page " + name + " in " + str(clock.stop()) + "s (Python+HTML)",module="debug")
|
||||
return html
|
||||
#return static_file("website/" + name + ".html",root="")
|
||||
#return static_file("web/" + name + ".html",root="")
|
||||
|
||||
|
||||
# Shortlinks
|
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 866 B |
|
@ -15,7 +15,8 @@ else{for(var key in data){body+=encodeURIComponent(key)+"="+encodeURIComponent(d
|
|||
xhttp.send(body);console.log("Sent XHTTP request to",url)}
|
||||
function xhttprequest(url,data={},method="GET",json=true){var p=new Promise(resolve=>xhttpreq(url,data,method,resolve,json));return p;}
|
||||
function now(){return Math.floor(Date.now()/1000);}
|
||||
return{getCookie:getCookie,setCookie:setCookie,getCookies:getCookies,saveCookies:saveCookies,xhttpreq:xhttpreq,xhttprequest:xhttprequest,now:now}}();document.addEventListener('DOMContentLoaded',function(){var elements=document.getElementsByClassName("seekable");for(var i=0;i<elements.length;i++){callback=elements[i].getAttribute("data-seekcallback");elements[i].addEventListener("click",function(evt){elmnt=evt.currentTarget;var percentage=evt.offsetX/elmnt.offsetWidth;elmnt.firstElementChild.style.width=(percentage*100)+"%";window[callback](percentage);})}
|
||||
return{getCookie:getCookie,setCookie:setCookie,getCookies:getCookies,saveCookies:saveCookies,xhttpreq:xhttpreq,xhttprequest:xhttprequest,now:now}}();document.addEventListener('DOMContentLoaded',function(){var elements=document.getElementsByClassName("seekable");for(var i=0;i<elements.length;i++){elements[i].addEventListener("click",function(evt){var elmnt=evt.currentTarget;var percentage=evt.offsetX/elmnt.offsetWidth;percentage=Math.max(0,Math.min(100,percentage));elmnt.firstElementChild.style.width=(percentage*100)+"%";var callback=elmnt.getAttribute("data-seekcallback");window[callback](percentage);})}
|
||||
var elements=document.getElementsByClassName("scrollseekable");for(var i=0;i<elements.length;i++){elements[i].addEventListener("wheel",function(evt){var elmnt=evt.currentTarget;var currentPercentage=elmnt.firstElementChild.offsetWidth/elmnt.offsetWidth;var sensitivity=elmnt.getAttribute("data-scrollsensitivity")||1;var percentage=currentPercentage-evt.deltaY*sensitivity/1000;percentage=Math.max(0,Math.min(1,percentage));elmnt.firstElementChild.style.width=(percentage*100)+"%";var callback=elmnt.getAttribute("data-seekcallback");window[callback](percentage);})}
|
||||
var elements2=document.getElementsByClassName("update");var functions=[]
|
||||
for(var i=0;i<elements2.length;i++){updatefunc=elements2[i].getAttribute("data-updatefrom");functions.push([elements2[i],updatefunc])}
|
||||
const SMOOTH_UPDATE=true;const update_delay=SMOOTH_UPDATE?40:500;function supervisor(){for(let entry of functions){var[element,func]=entry
|
|
@ -1,3 +1,5 @@
|
|||
localStorage = window.localStorage;
|
||||
|
||||
function showRange(identifier,unit) {
|
||||
// Make all modules disappear
|
||||
modules = document.getElementsByClassName("stat_module_" + identifier);
|
||||
|
@ -38,16 +40,25 @@ function showRange(identifier,unit) {
|
|||
|
||||
function showRangeManual(identifier,unit) {
|
||||
showRange(identifier,unit);
|
||||
neo.setCookie("rangeselect_" + identifier,unit);
|
||||
//neo.setCookie("rangeselect_" + identifier,unit);
|
||||
localStorage.setItem("rangeselect_" + identifier,unit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded',function() {
|
||||
for (let type of ["topartists","toptracks","pulse"]) {
|
||||
var val = neo.getCookie("rangeselect_" + type);
|
||||
if (val != undefined) {
|
||||
var val = localStorage.getItem("rangeselect_" + type);
|
||||
if (val != null) {
|
||||
showRange(type,val);
|
||||
}
|
||||
else {
|
||||
var val = neo.getCookie("rangeselect_" + type);
|
||||
if (val != undefined) {
|
||||
showRangeManual(type,val); //sets local storage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
|
@ -0,0 +1,102 @@
|
|||
|
||||
|
||||
function search(searchfield) {
|
||||
txt = searchfield.value;
|
||||
if (txt == "") {
|
||||
reallyclear()
|
||||
}
|
||||
else {
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = searchresult
|
||||
xhttp.open("GET","/api/search?max=5&query=" + encodeURIComponent(txt), true);
|
||||
xhttp.send();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function html_to_fragment(html) {
|
||||
var template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
return template.content;
|
||||
}
|
||||
|
||||
var results_artists;
|
||||
var results_tracks;
|
||||
var searchresultwrap;
|
||||
|
||||
window.addEventListener("DOMContentLoaded",function(){
|
||||
results_artists = document.getElementById("searchresults_artists");
|
||||
results_tracks = document.getElementById("searchresults_tracks");
|
||||
searchresultwrap = document.getElementById("resultwrap");
|
||||
});
|
||||
|
||||
var resulthtml = `
|
||||
<tr>
|
||||
<td class="image"></td>
|
||||
<td>
|
||||
<span></span><br/>
|
||||
<span></span>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
const oneresult = html_to_fragment(resulthtml).firstElementChild;
|
||||
|
||||
|
||||
|
||||
|
||||
function searchresult() {
|
||||
if (this.readyState == 4 && this.status == 200 && document.getElementById("searchinput").value != "") {
|
||||
// checking if field is empty in case we get an old result coming in that would overwrite our cleared result window
|
||||
var result = JSON.parse(this.responseText);
|
||||
var artists = result["artists"].slice(0,5)
|
||||
var tracks = result["tracks"].slice(0,5)
|
||||
|
||||
while (results_artists.firstChild) {
|
||||
results_artists.removeChild(results_artists.firstChild);
|
||||
}
|
||||
while (results_tracks.firstChild) {
|
||||
results_tracks.removeChild(results_tracks.firstChild);
|
||||
}
|
||||
|
||||
for (var i=0;i<artists.length;i++) {
|
||||
name = artists[i]["name"];
|
||||
link = artists[i]["link"];
|
||||
image = artists[i]["image"];
|
||||
|
||||
var node = oneresult.cloneNode(true);
|
||||
node.setAttribute("onclick","goto('" + link + "')");
|
||||
node.children[0].style.backgroundImage = "url('" + image + "')";
|
||||
node.children[1].children[0].innerHTML = name;
|
||||
|
||||
results_artists.appendChild(node);
|
||||
}
|
||||
for (var i=0;i<tracks.length;i++) {
|
||||
|
||||
artists = tracks[i]["artists"].join(", ");
|
||||
title = tracks[i]["title"];
|
||||
link = tracks[i]["link"];
|
||||
image = tracks[i]["image"];
|
||||
|
||||
var node = oneresult.cloneNode(true);
|
||||
node.setAttribute("onclick","goto('" + link + "')");
|
||||
node.children[0].style.backgroundImage = "url('" + image + "')";
|
||||
node.children[1].children[0].innerHTML = artists;
|
||||
node.children[1].children[2].innerHTML = title;
|
||||
|
||||
results_tracks.appendChild(node);
|
||||
}
|
||||
searchresultwrap.classList.remove("hide")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function clearresults() {
|
||||
window.setTimeout(reallyclear,500)
|
||||
}
|
||||
function reallyclear() {
|
||||
searchresultwrap.classList.add("hide")
|
||||
}
|
||||
|
||||
function goto(link) {
|
||||
window.location = link
|
||||
}
|
|
@ -132,3 +132,10 @@ div.grisons_bar:hover>div {
|
|||
a:hover {
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.hide {
|
||||
display:none;
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
@import "website/less/grisons";
|
||||
|
||||
body {
|
||||
padding:15px;
|
||||
padding-bottom:35px;
|
||||
|
@ -539,6 +537,8 @@ table.list td.bar div {
|
|||
background-color:@TEXT_COLOR;
|
||||
height:20px; /* can only do this absolute apparently */
|
||||
position:relative;
|
||||
display:inline-block;
|
||||
margin-bottom:-3px;
|
||||
}
|
||||
table.list tr:hover td.bar div {
|
||||
background-color:@FOCUS_COLOR;
|
||||
|
@ -698,6 +698,35 @@ table.tiles_3x3 td {
|
|||
width:33.333%;
|
||||
font-size:70%
|
||||
}
|
||||
table.tiles_4x4 td {
|
||||
font-size:50%
|
||||
}
|
||||
table.tiles_5x5 td {
|
||||
font-size:40%
|
||||
}
|
||||
|
||||
|
||||
.summary_rank {
|
||||
background-size:cover;
|
||||
background-position:center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.summary_rank_1 {
|
||||
width:300px;
|
||||
height:300px;
|
||||
border: 6px solid gold;
|
||||
}
|
||||
.summary_rank_2 {
|
||||
width:250px;
|
||||
height:250px;
|
||||
border: 4px solid silver;
|
||||
}
|
||||
.summary_rank_3 {
|
||||
width:240px;
|
||||
height:240px;
|
||||
border: 3px solid #cd7f32;
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 239 B After Width: | Height: | Size: 239 B |
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
@ -1,10 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import setproctitle
|
||||
import signal
|
||||
from doreah.logging import log
|
||||
from doreah.settings import get_settings
|
||||
|
||||
|
||||
setproctitle.setproctitle("maloja_supervisor")
|
||||
|
@ -16,10 +18,15 @@ while True:
|
|||
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")
|
||||
if get_settings("UPDATE_AFTER_CRASH"):
|
||||
log("Updating first...",module="supervisor")
|
||||
try:
|
||||
os.system("pip3 install maloja --upgrade --no-cache-dir")
|
||||
except:
|
||||
log("Could not update.",module="supervisor")
|
||||
try:
|
||||
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
except e:
|
||||
log("Error starting Maloja: " + str(e),module="supervisor")
|
|
@ -1,6 +1,6 @@
|
|||
import urllib
|
||||
from bottle import FormsDict
|
||||
from malojatime import time_fix, time_str, get_range_object
|
||||
from .malojatime import time_fix, time_str, get_range_object
|
||||
import math
|
||||
|
||||
# necessary because urllib.parse.urlencode doesnt handle multidicts
|
|
@ -14,7 +14,10 @@ from doreah import caching
|
|||
from doreah.logging import log
|
||||
from doreah.regular import yearly, daily
|
||||
|
||||
from external import api_request_track, api_request_artist
|
||||
from .external import api_request_track, api_request_artist
|
||||
from .__init__ import version
|
||||
from . import globalconf
|
||||
from .globalconf import datadir
|
||||
|
||||
|
||||
|
||||
|
@ -125,10 +128,20 @@ def consistentRulestate(folder,checksums):
|
|||
#####
|
||||
|
||||
|
||||
if globalconf.USE_THUMBOR:
|
||||
def thumborize(url):
|
||||
if url.startswith("/"): url = globalconf.OWNURL + url
|
||||
encrypted_url = globalconf.THUMBOR_GENERATOR.generate(
|
||||
width=300,
|
||||
height=300,
|
||||
smart=True,
|
||||
image_url=url
|
||||
)
|
||||
return globalconf.THUMBOR_SERVER + encrypted_url
|
||||
|
||||
|
||||
|
||||
|
||||
else:
|
||||
def thumborize(url):
|
||||
return url
|
||||
|
||||
|
||||
|
||||
|
@ -217,12 +230,12 @@ def local_files(artist=None,artists=None,title=None):
|
|||
# direct files
|
||||
for ext in ["png","jpg","jpeg","gif"]:
|
||||
#for num in [""] + [str(n) for n in range(0,10)]:
|
||||
if os.path.exists(purename + "." + ext):
|
||||
if os.path.exists(datadir(purename + "." + ext)):
|
||||
images.append("/" + purename + "." + ext)
|
||||
|
||||
# folder
|
||||
try:
|
||||
for f in os.listdir(purename + "/"):
|
||||
for f in os.listdir(datadir(purename)):
|
||||
if f.split(".")[-1] in ["png","jpg","jpeg","gif"]:
|
||||
images.append("/" + purename + "/" + f)
|
||||
except:
|
||||
|
@ -239,11 +252,6 @@ local_track_cache = caching.Cache(maxage=local_cache_age)
|
|||
|
||||
def getTrackImage(artists,title,fast=False):
|
||||
|
||||
# obj = (frozenset(artists),title)
|
||||
# filename = "-".join([re.sub("[^a-zA-Z0-9]","",artist) for artist in sorted(artists)]) + "_" + re.sub("[^a-zA-Z0-9]","",title)
|
||||
# if filename == "": filename = str(hash(obj))
|
||||
# filepath = "images/tracks/" + filename
|
||||
|
||||
if settings.get_settings("USE_LOCAL_IMAGES"):
|
||||
|
||||
try:
|
||||
|
@ -257,21 +265,6 @@ def getTrackImage(artists,title,fast=False):
|
|||
return urllib.parse.quote(res)
|
||||
|
||||
|
||||
# check if custom image exists
|
||||
# if os.path.exists(filepath + ".png"):
|
||||
# imgurl = "/" + filepath + ".png"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".jpg"):
|
||||
# imgurl = "/" + filepath + ".jpg"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".jpeg"):
|
||||
# imgurl = "/" + filepath + ".jpeg"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".gif"):
|
||||
# imgurl = "/" + filepath + ".gif"
|
||||
# return imgurl
|
||||
|
||||
|
||||
try:
|
||||
# check our cache
|
||||
# if we have cached the nonexistence of that image, we immediately return the redirect to the artist and let the resolver handle it
|
||||
|
@ -313,47 +306,31 @@ def getTrackImage(artists,title,fast=False):
|
|||
|
||||
def getArtistImage(artist,fast=False):
|
||||
|
||||
# obj = artist
|
||||
# filename = re.sub("[^a-zA-Z0-9]","",artist)
|
||||
# if filename == "": filename = str(hash(obj))
|
||||
# filepath = "images/artists/" + filename
|
||||
# #filepath_cache = "info/artists_cache/" + filename
|
||||
|
||||
if settings.get_settings("USE_LOCAL_IMAGES"):
|
||||
|
||||
try:
|
||||
return local_artist_cache.get(artist)
|
||||
return thumborize(local_artist_cache.get(artist))
|
||||
# Local cached image
|
||||
except:
|
||||
# Get all local images, select one if present
|
||||
images = local_files(artist=artist)
|
||||
if len(images) != 0:
|
||||
#return random.choice(images)
|
||||
res = random.choice(images)
|
||||
local_artist_cache.add(artist,res)
|
||||
return urllib.parse.quote(res)
|
||||
|
||||
|
||||
# check if custom image exists
|
||||
# if os.path.exists(filepath + ".png"):
|
||||
# imgurl = "/" + filepath + ".png"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".jpg"):
|
||||
# imgurl = "/" + filepath + ".jpg"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".jpeg"):
|
||||
# imgurl = "/" + filepath + ".jpeg"
|
||||
# return imgurl
|
||||
# elif os.path.exists(filepath + ".gif"):
|
||||
# imgurl = "/" + filepath + ".gif"
|
||||
# return imgurl
|
||||
return thumborize(urllib.parse.quote(res))
|
||||
|
||||
|
||||
# if no local images (or setting to not use them)
|
||||
try:
|
||||
#result = cachedArtists[artist]
|
||||
result = artist_cache.get(artist) #artist_from_cache(artist)
|
||||
if result is not None: return result
|
||||
# check cache for foreign image
|
||||
result = artist_cache.get(artist)
|
||||
if result is not None: return thumborize(result)
|
||||
else: return ""
|
||||
# none means non-existence is cached, return empty
|
||||
except:
|
||||
pass
|
||||
# no cache entry, go on
|
||||
|
||||
|
||||
|
||||
|
@ -362,7 +339,6 @@ def getArtistImage(artist,fast=False):
|
|||
# if apikey is None: return "" # DO NOT CACHE THAT
|
||||
|
||||
|
||||
|
||||
# fast request only retuns cached and local results, generates redirect link for rest
|
||||
if fast: return "/image?artist=" + urllib.parse.quote(artist)
|
||||
|
||||
|
@ -373,7 +349,7 @@ def getArtistImage(artist,fast=False):
|
|||
#cachedArtists[artist] = result
|
||||
artist_cache.add(artist,result) #cache_artist(artist,result)
|
||||
|
||||
if result is not None: return result
|
||||
if result is not None: return thumborize(result)
|
||||
else: return ""
|
||||
|
||||
def getTrackImages(trackobjectlist,fast=False):
|
||||
|
@ -469,7 +445,7 @@ def set_image(b64,**keys):
|
|||
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
|
||||
|
||||
currentyear = datetime.datetime.utcnow().year
|
||||
try:
|
||||
|
@ -505,8 +481,8 @@ def update_medals():
|
|||
@daily
|
||||
def update_weekly():
|
||||
|
||||
from database import WEEKLY_TOPTRACKS, WEEKLY_TOPARTISTS, get_charts_artists, get_charts_tracks
|
||||
from malojatime import ranges, thisweek
|
||||
from .database import WEEKLY_TOPTRACKS, WEEKLY_TOPARTISTS, get_charts_artists, get_charts_tracks
|
||||
from .malojatime import ranges, thisweek
|
||||
|
||||
|
||||
WEEKLY_TOPARTISTS.clear()
|
||||
|
@ -521,3 +497,29 @@ def update_weekly():
|
|||
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
|
||||
|
||||
|
||||
@daily
|
||||
def send_stats():
|
||||
if settings.get_settings("SEND_STATS"):
|
||||
|
||||
log("Sending daily stats report...")
|
||||
|
||||
from .database import ARTISTS, TRACKS, SCROBBLES
|
||||
|
||||
keys = {
|
||||
"url":"https://myrcella.krateng.ch/malojastats",
|
||||
"method":"POST",
|
||||
"headers":{"Content-Type": "application/json"},
|
||||
"data":json.dumps({
|
||||
"name":settings.get_settings("NAME"),
|
||||
"url":settings.get_settings("PUBLIC_URL"),
|
||||
"version":".".join(str(d) for d in version),
|
||||
"artists":len(ARTISTS),
|
||||
"tracks":len(TRACKS),
|
||||
"scrobbles":len(SCROBBLES)
|
||||
}).encode("utf-8")
|
||||
}
|
||||
req = urllib.request.Request(**keys)
|
||||
response = urllib.request.urlopen(req)
|
||||
log("Sent daily report!")
|
|
@ -5,14 +5,25 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja</title>
|
||||
<script src="javascript/cookies.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<pyhp include="common/header.html" />
|
||||
|
||||
<script>
|
||||
neo.xhttprequest("https://maloja.krateng.ch/api/serverinfo",{},"GET",json=true).then((response)=>{
|
||||
|
||||
function versioncompare(a,b) {
|
||||
for (var pos=0;pos<3;pos++) {
|
||||
var v1 = parseInt(a[pos]) || 0;
|
||||
var v2 = parseInt(b[pos]) || 0;
|
||||
if (v1 > v2) { return 1;}
|
||||
if (v1 < v2) { return -1;}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
neo.xhttprequest("https://pypi.org/pypi/malojaserver/json",{},"GET",json=true).then((response)=>{
|
||||
result = JSON.parse(response.responseText);
|
||||
latestvers = result.version;
|
||||
latestvers = result.info.version.split(".");
|
||||
|
||||
neo.xhttprequest("/api/serverinfo",{},"GET",json=true).then((response)=>{
|
||||
|
||||
|
@ -22,7 +33,7 @@
|
|||
document.getElementById("latestversion").innerHTML = latestvers.join(".");
|
||||
document.getElementById("currentversion").innerHTML = thisvers.join(".");
|
||||
|
||||
if (latestvers <= thisvers) {
|
||||
if (versioncompare(latestvers,thisvers) <= 0) {
|
||||
document.getElementById("currentversion").style.color = "green";
|
||||
}
|
||||
else {
|
||||
|
@ -96,7 +107,12 @@
|
|||
<h2>Links</h2>
|
||||
|
||||
<a class="textlink" href="/setup">Server Setup</a><br/>
|
||||
<a class="textlink" href="/manual">Manual Scrobbling</a>
|
||||
<a class="textlink" href="/manual">Manual Scrobbling</a><br/>
|
||||
<a class="textlink" href="/issues">Database Maintenance</a>
|
||||
|
||||
<h2>External</h2>
|
||||
|
||||
<a class="textlink" href="https://github.com/krateng/maloja/issues/new">Report Issue</a><br/>
|
||||
|
||||
<pyhp include="common/footer.html" />
|
||||
</body>
|
|
@ -4,8 +4,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - KEY_ARTISTNAME</title>
|
||||
<script src="javascript/cookies.js"></script>
|
||||
<script src="javascript/rangeselect.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
<script src="/rangeselect.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
|
@ -1,13 +1,13 @@
|
|||
import urllib
|
||||
import database
|
||||
from malojatime import today,thisweek,thismonth,thisyear
|
||||
from .. import database
|
||||
from ..malojatime import today,thisweek,thismonth,thisyear
|
||||
|
||||
|
||||
def instructions(keys):
|
||||
from utilities import getArtistImage
|
||||
from htmlgenerators import artistLink, artistLinks, link_address
|
||||
from urihandler import compose_querystring, uri_to_internal
|
||||
from htmlmodules import module_pulse, module_performance, module_trackcharts, module_scrobblelist
|
||||
from ..utilities import getArtistImage
|
||||
from ..htmlgenerators import artistLink, artistLinks, link_address
|
||||
from ..urihandler import compose_querystring, uri_to_internal
|
||||
from ..htmlmodules import module_pulse, module_performance, module_trackcharts, module_scrobblelist
|
||||
|
||||
filterkeys, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
||||
artist = filterkeys.get("artist")
|
|
@ -37,7 +37,7 @@
|
|||
<title>Maloja - <pyhp echo="artist" /></title>
|
||||
|
||||
<pyhp include="common/header.html" />
|
||||
<script src="javascript/rangeselect.js"></script>
|
||||
<script src="/rangeselect.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -65,7 +65,7 @@
|
|||
|
||||
<!-- TOPWEEKS -->
|
||||
<span><pyhp if="info.get('topweeks') not in [0,None]">
|
||||
<a title="{str(info['topweeks'])} weeks on #1" href="/performance?{encodedartist}">
|
||||
<a title="{str(info['topweeks'])} weeks on #1" href="/performance?{encodedartist}&step=week">
|
||||
<img class="star" src="/media/star.png" /><pyhp echo="info['topweeks']" />
|
||||
</a>
|
||||
</pyhp></span>
|
||||
|
@ -103,24 +103,19 @@
|
|||
<h2><a href='/pulse?{encodedartist}&step=year&trail=1'>Pulse</a></h2>
|
||||
|
||||
<pyhp for="range" in="ranges" separator = " | ">
|
||||
<pyhp if="initialrange==range[0]">
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}" style="opacity:0.5;"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
<pyhp if="initialrange!=range[0]">
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}" style="{'opacity:0.5;' if initialrange==range[0] else ''}"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<pyhp for="range" in="ranges">
|
||||
<pyhp if="initialrange==range[0]">
|
||||
<span class="stat_module_pulse pulse_{range[0]}"><pyhp echo="htmlmodules.module_pulse(artist=artist,max_=range[4],since=range[2],step=range[3],trail=1)" /></span>
|
||||
</pyhp>
|
||||
<pyhp if="initialrange!=range[0]">
|
||||
|
||||
<span class="stat_module_pulse pulse_{range[0]}" style="display:none;"><pyhp echo="htmlmodules.module_pulse(artist=artist,max_=range[4],since=range[2],step=range[3],trail=1)" /></span>
|
||||
</pyhp>
|
||||
<span class="stat_module_pulse pulse_{range[0]}" style="{'display:none;' if initialrange==range[0] else ''}">
|
||||
<!--<pyhp echo="htmlmodules.module_pulse(artist=artist,max_=range[4],since=range[2],step=range[3],trail=1)" />-->
|
||||
|
||||
<pyhp include="partial/pulse.pyhp" with="{'amountkeys':{'max_':range[4]},'limitkeys':{'since':range[2]},'delimitkeys':{'step':range[3],'trail':1}}" />
|
||||
|
||||
</span>
|
||||
|
||||
</pyhp>
|
||||
</td>
|
||||
|
@ -129,24 +124,17 @@
|
|||
<h2><a href='/performance?{encodedcredited}&step=year&trail=1'>Performance</a></h2>
|
||||
|
||||
<pyhp for="range" in="ranges" separator = " | ">
|
||||
<pyhp if="initialrange==range[0]">
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}" style="opacity:0.5;"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
<pyhp if="initialrange!=range[0]">
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
|
||||
<span onclick="showRangeManual('pulse','{range[0]}')" class="stat_selector_pulse selector_pulse_{range[0]}" style="{'opacity:0.5;' if initialrange==range[0] else ''}"><pyhp echo="range[1]" /></span>
|
||||
</pyhp>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<pyhp for="range" in="ranges">
|
||||
<pyhp if="initialrange==range[0]">
|
||||
<span class="stat_module_pulse pulse_{range[0]}"><pyhp echo="htmlmodules.module_performance(artist=credited,max_=range[4],since=range[2],step=range[3],trail=1)" /></span>
|
||||
</pyhp>
|
||||
<pyhp if="initialrange!=range[0]">
|
||||
<span class="stat_module_pulse pulse_{range[0]}" style="display:none;"><pyhp echo="htmlmodules.module_performance(artist=credited,max_=range[4],since=range[2],step=range[3],trail=1)" /></span>
|
||||
</pyhp>
|
||||
|
||||
<span class="stat_module_pulse pulse_{range[0]}" style="{'display:none;' if initialrange==range[0] else ''}">
|
||||
<!--<pyhp echo="htmlmodules.module_performance(artist=credited,max_=range[4],since=range[2],step=range[3],trail=1)" />-->
|
||||
<pyhp include="partial/performance.pyhp" with="{'filterkeys':{'artist':credited},'amountkeys':{'max_':range[4]},'limitkeys':{'since':range[2]},'delimitkeys':{'step':range[3],'trail':1}}" />
|
||||
</span>
|
||||
|
||||
</pyhp>
|
||||
</td>
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Artist Charts</title>
|
||||
<script src="javascript/datechange.js" async></script>
|
||||
<script src="/datechange.js" async></script>
|
||||
</head>
|
||||
|
||||
<body>
|
|
@ -2,10 +2,10 @@ import urllib
|
|||
|
||||
|
||||
def instructions(keys):
|
||||
from utilities import getArtistImage
|
||||
from urihandler import compose_querystring, uri_to_internal
|
||||
from htmlmodules import module_artistcharts, module_filterselection, module_artistcharts_tiles
|
||||
from malojatime import range_desc
|
||||
from ..utilities import getArtistImage
|
||||
from ..urihandler import compose_querystring, uri_to_internal
|
||||
from ..htmlmodules import module_artistcharts, module_filterselection, module_artistcharts_tiles
|
||||
from ..malojatime import range_desc
|
||||
from doreah.settings import get_settings
|
||||
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Artist Charts</title>
|
||||
<script src="/datechange.js" async></script>
|
||||
|
||||
<pyhp include="common/header.html" />
|
||||
</head>
|
||||
|
||||
<pyhp>
|
||||
try:
|
||||
top = db.get_charts_artists(**filterkeys,**limitkeys)[0]["artist"]
|
||||
img = utilities.getArtistImage(top)
|
||||
except:
|
||||
img = ""
|
||||
</pyhp>
|
||||
|
||||
<body>
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{img}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
|
||||
<span><pyhp echo="limitkeys['timerange'].desc(prefix=True)" /></span>
|
||||
<br/><br/>
|
||||
<pyhp echo="htmlmodules.module_filterselection(_urikeys)" />
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<span class="stat_module_topartists">
|
||||
<pyhp if="settings('CHARTS_DISPLAY_TILES')">
|
||||
<pyhp include="partial/charts_artists_tiles.pyhp" />
|
||||
</pyhp>
|
||||
|
||||
</span>
|
||||
|
||||
<pyhp include="partial/charts_artists.pyhp" />
|
||||
|
||||
<pyhp include="common/footer.html" />
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -2,11 +2,11 @@ import urllib
|
|||
|
||||
|
||||
def instructions(keys):
|
||||
from utilities import getArtistImage, getTrackImage
|
||||
from htmlgenerators import artistLink
|
||||
from urihandler import compose_querystring, uri_to_internal
|
||||
from htmlmodules import module_trackcharts, module_filterselection, module_trackcharts_tiles
|
||||
from malojatime import range_desc
|
||||
from ..utilities import getArtistImage, getTrackImage
|
||||
from ..htmlgenerators import artistLink
|
||||
from ..urihandler import compose_querystring, uri_to_internal
|
||||
from ..htmlmodules import module_trackcharts, module_filterselection, module_trackcharts_tiles
|
||||
from ..malojatime import range_desc
|
||||
from doreah.settings import get_settings
|
||||
|
||||
filterkeys, timekeys, _, amountkeys = uri_to_internal(keys)
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Track Charts</title>
|
||||
<script src="/datechange.js" async></script>
|
||||
|
||||
<pyhp include="common/header.html" />
|
||||
</head>
|
||||
|
||||
<pyhp>
|
||||
try:
|
||||
try:
|
||||
img = utilities.getArtistImage(filterkeys['artist'])
|
||||
except:
|
||||
top = db.get_charts_tracks(**filterkeys,**limitkeys)[0]["track"]
|
||||
img = utilities.getTrackImage(**top)
|
||||
except:
|
||||
img = ""
|
||||
</pyhp>
|
||||
|
||||
<body>
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{img}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Track Charts</h1><a href="/top_tracks"><span>View #1 Tracks</span></a><br/>
|
||||
<span>
|
||||
<pyhp if="filterkeys.get('artist') is not None">by <pyhp echo="htmlgenerators.artistLink(filterkeys.get('artist'))" /></pyhp>
|
||||
<pyhp echo="limitkeys['timerange'].desc(prefix=True)" />
|
||||
</span>
|
||||
<br/><br/>
|
||||
<pyhp echo="htmlmodules.module_filterselection(_urikeys)" />
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<span class="stat_module_toptracks">
|
||||
<pyhp if="settings('CHARTS_DISPLAY_TILES')">
|
||||
<pyhp include="partial/charts_tracks_tiles.pyhp" />
|
||||
</pyhp>
|
||||
|
||||
</span>
|
||||
|
||||
<pyhp include="partial/charts_tracks.pyhp" />
|
||||
|
||||
<pyhp include="common/footer.html" />
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -9,7 +9,17 @@
|
|||
<span><input id="searchinput" placeholder="Search for an artist or track..." oninput="search(this)" onblur="clearresults()" /></span>
|
||||
</div>
|
||||
|
||||
<span id="resultwrap"></span>
|
||||
<span id="resultwrap" class="hide">
|
||||
<div class="searchresults">
|
||||
<span>Artists</span>
|
||||
<table class="searchresults_artists" id="searchresults_artists">
|
||||
</table>
|
||||
<br/><br/>
|
||||
<span>Tracks</span>
|
||||
<table class="searchresults_tracks" id="searchresults_tracks">
|
||||
</table>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="/admin"><div title="Server Administration" id="settingsicon">
|
|
@ -5,7 +5,7 @@
|
|||
<link rel="stylesheet" href="/css/maloja.css" />
|
||||
<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>
|
||||
<script src="/javascript/neopolitan.js"></script>
|
||||
<script src="/javascript/upload.js"></script>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<script src="/search.js"></script>
|
||||
<script src="/neopolitan.js"></script>
|
||||
<script src="/upload.js"></script>
|
|
@ -1,8 +1,8 @@
|
|||
import urllib
|
||||
import database
|
||||
from .. import database
|
||||
import json
|
||||
from htmlgenerators import artistLink
|
||||
from utilities import getArtistImage
|
||||
from ..htmlgenerators import artistLink
|
||||
from ..utilities import getArtistImage
|
||||
|
||||
|
||||
def instructions(keys):
|
||||
|
@ -15,6 +15,8 @@ def instructions(keys):
|
|||
|
||||
owninfo = database.info()
|
||||
|
||||
database.add_known_server(compareto)
|
||||
|
||||
artists = {}
|
||||
|
||||
for a in owninfo["artists"]:
|
|
@ -3,7 +3,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Error ERROR_CODE</title>
|
||||
<title>Maloja - Error <pyhp echo="errorcode" /></title>
|
||||
<pyhp include="../common/header.html" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -13,7 +14,7 @@
|
|||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Error ERROR_CODE</h1><br/>
|
||||
<h1>Error <pyhp echo="errorcode" /></h1><br/>
|
||||
|
||||
|
||||
<p>That did not work. Don't ask me why.</p>
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Issues</title>
|
||||
<script src="javascript/cookies.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/db/newrule?", true);
|
||||
xhttp.open("POST","/api/newrule?", true);
|
||||
xhttp.send(keys);
|
||||
e = arguments[0]
|
||||
line = e.parentNode
|
||||
|
@ -56,7 +56,7 @@
|
|||
apikey = document.getElementById("apikey").value
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/db/rebuild", true);
|
||||
xhttp.open("POST","/api/rebuild", true);
|
||||
xhttp.send("key=" + encodeURIComponent(apikey))
|
||||
window.location = "/wait";
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import urllib
|
||||
import database
|
||||
from htmlgenerators import artistLink
|
||||
from .. import database
|
||||
from ..htmlgenerators import artistLink
|
||||
|
||||
def instructions(keys):
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja</title>
|
||||
<script src="javascript/cookies.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<script>
|
||||
function keyDetect(event) {
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
<pyhp>
|
||||
if amountkeys.get("max_") is not None: amountkeys["perpage"],amountkeys["page"] = amountkeys["max_"],0
|
||||
firstindex = amountkeys["page"] * amountkeys["perpage"]
|
||||
lastindex = firstindex + amountkeys["perpage"]
|
||||
</pyhp>
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
<pyhp save="db.get_charts_artists(**filterkeys,**limitkeys)" as="artists" />
|
||||
<pyhp save="math.ceil(len(artists) / amountkeys['perpage'])" as="pages" />
|
||||
|
||||
|
||||
<pyhp>
|
||||
try:
|
||||
artistslast = db.get_charts_artists(**filterkeys,timerange=limitkeys["timerange"].next(step=-1))
|
||||
# create rank association
|
||||
lastrank = {}
|
||||
for al in artistslast:
|
||||
lastrank[al["artist"]] = al["rank"]
|
||||
for a in artists:
|
||||
try:
|
||||
a["delta"] = lastrank[a["artist"]] - a["rank"]
|
||||
except:
|
||||
a["delta"] = math.inf
|
||||
except:
|
||||
pass
|
||||
</pyhp>
|
||||
|
||||
<!--
|
||||
if artists != []:
|
||||
maxbar = artists[0]["scrobbles"]
|
||||
representative = artists[0]["artist"]
|
||||
else:
|
||||
representative = None
|
||||
-->
|
||||
<pyhp save="artists[0]['scrobbles'] if artists != [] else 0" as="maxbar" />
|
||||
|
||||
<table class='list'>
|
||||
<pyhp save="True" as="first" />
|
||||
<pyhp for="e" in="artists[firstindex:lastindex]">
|
||||
<tr>
|
||||
<!-- Rank -->
|
||||
<td class="rank"><pyhp if="first or e['scrobbles'] < prev['scrobbles']">#<pyhp echo="e['rank']"/></pyhp></td>
|
||||
<pyhp save="False" as="first" />
|
||||
<!-- Rank change -->
|
||||
<pyhp if="e.get('delta') is not None">
|
||||
<pyhp if="e.get('delta') is math.inf">
|
||||
<td class='rankup' title='New'>🆕</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') > 0 and e.get('delta') is not math.inf">
|
||||
<td class='rankup' title="up from #{e['rank']+e['delta']}">↗</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') < 0">
|
||||
<td class='rankdown' title="down from #{e['rank']+e['delta']}">↘</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') == 0">
|
||||
<td class='ranksame' title="Unchanged">➡</td>
|
||||
</pyhp>
|
||||
</pyhp>
|
||||
|
||||
<!-- artist -->
|
||||
<pyhp echo="htmlgenerators.entity_column(e['artist'],counting=e['counting'])" />
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount"><pyhp echo="htmlgenerators.scrobblesArtistLink(e['artist'],urihandler.internal_to_uri(limitkeys),amount=e['scrobbles'],associated=True)" /></td>
|
||||
<td class="bar"><pyhp echo="htmlgenerators.scrobblesArtistLink(e['artist'],urihandler.internal_to_uri(limitkeys),percent=e['scrobbles']*100/maxbar,associated=True)" /></td>
|
||||
</tr>
|
||||
<pyhp save="e" as="prev" />
|
||||
</pyhp>
|
||||
</table>
|
||||
|
||||
|
||||
<pyhp if="amountkeys.get('max_') is None">
|
||||
<pyhp include="pagination.pyhp" />
|
||||
</pyhp>
|
|
@ -0,0 +1,49 @@
|
|||
<pyhp>
|
||||
num_levels = 3
|
||||
</pyhp>
|
||||
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
|
||||
<pyhp save="db.get_charts_artists(**filterkeys,**limitkeys)" as="artists" />
|
||||
<pyhp save="iter(artists)" as="artists_iterator" />
|
||||
|
||||
|
||||
|
||||
<table class="tiles_top">
|
||||
<tr>
|
||||
<pyhp for="level" in="range(1,num_levels+1)">
|
||||
<td><table class="tiles_{level}x{level} tiles_sub">
|
||||
<pyhp for="row" in="range(level)">
|
||||
<tr>
|
||||
<pyhp for="column" in="range(level)">
|
||||
<pyhp>
|
||||
try:
|
||||
entry = next(artists_iterator)
|
||||
artist = entry['artist']
|
||||
rank = entry['rank']
|
||||
except:
|
||||
artist = None
|
||||
</pyhp>
|
||||
<pyhp if="artist is None">
|
||||
<td><span class='stats'></span> <span></span></td>
|
||||
</pyhp>
|
||||
<pyhp if="artist is not None">
|
||||
<td onclick='window.location.href="{htmlgenerators.link_address(artist)}"'
|
||||
style='cursor:pointer;background-image:url("{utilities.getArtistImage(artist,fast=True)}");'>
|
||||
<span class='stats'>
|
||||
#<pyhp echo="rank" />
|
||||
</span>
|
||||
<span>
|
||||
<pyhp echo="htmlgenerators.html_link(artist)" />
|
||||
</span>
|
||||
</td>
|
||||
</pyhp>
|
||||
</pyhp>
|
||||
</tr>
|
||||
</pyhp>
|
||||
</table></td>
|
||||
</pyhp>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,70 @@
|
|||
|
||||
<pyhp>
|
||||
if amountkeys.get("max_") is not None: amountkeys["perpage"],amountkeys["page"] = amountkeys["max_"],0
|
||||
firstindex = amountkeys["page"] * amountkeys["perpage"]
|
||||
lastindex = firstindex + amountkeys["perpage"]
|
||||
</pyhp>
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
<pyhp save="db.get_charts_tracks(**filterkeys,**limitkeys)" as="tracks" />
|
||||
<pyhp save="math.ceil(len(tracks) / amountkeys['perpage'])" as="pages" />
|
||||
|
||||
|
||||
<pyhp>
|
||||
try:
|
||||
trackslast = db.get_charts_tracks(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1))
|
||||
# create rank association
|
||||
lastrank = {}
|
||||
for tl in trackslast:
|
||||
lastrank[(*tl["track"]["artists"],tl["track"]["title"])] = tl["rank"]
|
||||
for t in tracks:
|
||||
try:
|
||||
t["delta"] = lastrank[(*t["track"]["artists"],t["track"]["title"])] - t["rank"]
|
||||
except:
|
||||
t["delta"] = math.inf
|
||||
except:
|
||||
pass
|
||||
</pyhp>
|
||||
|
||||
|
||||
<pyhp save="tracks[0]['scrobbles'] if tracks != [] else 0" as="maxbar" />
|
||||
|
||||
<table class='list'>
|
||||
<pyhp save="True" as="first" />
|
||||
<pyhp for="e" in="tracks[firstindex:lastindex]">
|
||||
<tr>
|
||||
<!-- Rank -->
|
||||
<td class="rank"><pyhp if="first or e['scrobbles'] < prev['scrobbles']">#<pyhp echo="e['rank']"/></pyhp></td>
|
||||
<pyhp save="False" as="first" />
|
||||
<!-- Rank change -->
|
||||
<pyhp if="e.get('delta') is not None">
|
||||
<pyhp if="e.get('delta') is math.inf">
|
||||
<td class='rankup' title='New'>🆕</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') > 0 and e.get('delta') is not math.inf">
|
||||
<td class='rankup' title="up from #{e['rank']+e['delta']}">↗</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') < 0">
|
||||
<td class='rankdown' title="down from #{e['rank']+e['delta']}">↘</td>
|
||||
</pyhp>
|
||||
<pyhp if="e.get('delta') == 0">
|
||||
<td class='ranksame' title="Unchanged">➡</td>
|
||||
</pyhp>
|
||||
</pyhp>
|
||||
|
||||
<!-- artist -->
|
||||
<pyhp echo="htmlgenerators.entity_column(e['track'])" />
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount"><pyhp echo="htmlgenerators.scrobblesTrackLink(e['track'],urihandler.internal_to_uri(limitkeys),amount=e['scrobbles'])" /></td>
|
||||
<td class="bar"><pyhp echo="htmlgenerators.scrobblesTrackLink(e['track'],urihandler.internal_to_uri(limitkeys),percent=e['scrobbles']*100/maxbar)" /></td>
|
||||
</tr>
|
||||
<pyhp save="e" as="prev" />
|
||||
</pyhp>
|
||||
</table>
|
||||
|
||||
|
||||
<pyhp if="amountkeys.get('max_') is None">
|
||||
<pyhp include="pagination.pyhp" />
|
||||
</pyhp>
|
|
@ -0,0 +1,51 @@
|
|||
<pyhp>
|
||||
num_levels = 3
|
||||
</pyhp>
|
||||
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
|
||||
<pyhp save="db.get_charts_tracks(**filterkeys,**limitkeys)" as="tracks" />
|
||||
<pyhp save="iter(tracks)" as="tracks_iterator" />
|
||||
|
||||
|
||||
|
||||
<table class="tiles_top">
|
||||
<tr>
|
||||
<pyhp for="level" in="range(1,num_levels+1)">
|
||||
<td><table class="tiles_{level}x{level} tiles_sub">
|
||||
<pyhp for="row" in="range(level)">
|
||||
<tr>
|
||||
<pyhp for="column" in="range(level)">
|
||||
<pyhp>
|
||||
try:
|
||||
entry = next(tracks_iterator)
|
||||
track = entry['track']
|
||||
artists = track['artists']
|
||||
title = track['title']
|
||||
rank = entry['rank']
|
||||
except:
|
||||
track = None
|
||||
</pyhp>
|
||||
<pyhp if="track is None">
|
||||
<td><span class='stats'></span> <span></span></td>
|
||||
</pyhp>
|
||||
<pyhp if="track is not None">
|
||||
<td onclick='window.location.href="{htmlgenerators.link_address(track)}"'
|
||||
style='cursor:pointer;background-image:url("{utilities.getTrackImage(artists,title,fast=True)}");'>
|
||||
<span class='stats'>
|
||||
#<pyhp echo="rank" />
|
||||
</span>
|
||||
<span>
|
||||
<pyhp echo="htmlgenerators.html_link(track)" />
|
||||
</span>
|
||||
</td>
|
||||
</pyhp>
|
||||
</pyhp>
|
||||
</tr>
|
||||
</pyhp>
|
||||
</table></td>
|
||||
</pyhp>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,8 @@
|
|||
<a href='/scrobbles?{uri_query(track)}&{compose_querystring(timekeys)}'>
|
||||
<pyhp if="amount is not None">
|
||||
<pyhp echo="amount" />
|
||||
</pyhp>
|
||||
<pyhp if="amount is None">
|
||||
<div style='width:{percent}%;'></div>
|
||||
</pyhp>
|
||||
</a>
|
|
@ -0,0 +1,36 @@
|
|||
<pyhp save="{'perpage':amountkeys['perpage'],**filterkeys,**limitkeys,**delimitkeys}" as="unchanged" />
|
||||
|
||||
<pyhp save="amountkeys['page']" as="page" />
|
||||
|
||||
<pyhp save="urihandler.compose_querystring" as="compose" />
|
||||
<pyhp save="urihandler.internal_to_uri" as="touri" />
|
||||
|
||||
<div class='paginate'>
|
||||
|
||||
<pyhp if="pages > 1">
|
||||
<pyhp if="page > 1">
|
||||
<a href='?{compose(touri(dict(**unchanged,page=0)))}'><span class='stat_selector'>1</span></a> |
|
||||
</pyhp>
|
||||
|
||||
<pyhp if="page > 2"> ... | </pyhp>
|
||||
|
||||
<pyhp if="page > 0">
|
||||
<a href='?{compose(touri(dict(**unchanged,page=page-1)))}'><span class='stat_selector'><pyhp echo="page" /></span></a> «
|
||||
</pyhp>
|
||||
|
||||
<span style='opacity:0.5;' class='stat_selector'>
|
||||
<pyhp echo="page+1" />
|
||||
</span>
|
||||
|
||||
<pyhp if="page < pages - 1">
|
||||
» <a href='?{compose(touri(dict(**unchanged,page=page+1)))}'><span class='stat_selector'><pyhp echo="page+2" /></span></a>
|
||||
</pyhp>
|
||||
|
||||
<pyhp if="page < pages - 3"> | ... </pyhp>
|
||||
|
||||
<pyhp if="page < pages - 2">
|
||||
| <a href='?{compose(touri(dict(**unchanged,page=pages-1)))}'><span class='stat_selector'><pyhp echo="pages" /></span></a>
|
||||
</pyhp>
|
||||
</pyhp>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
<pyhp>
|
||||
if amountkeys["max_"] is not None: amountkeys["perpage"],amountkeys["page"] = amountkeys["max_"],0
|
||||
firstindex = amountkeys["page"] * amountkeys["perpage"]
|
||||
lastindex = firstindex + amountkeys["perpage"]
|
||||
</pyhp>
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
<pyhp save="db.get_performance(**limitkeys,**delimitkeys,**filterkeys)" as="allranges" />
|
||||
<pyhp save="math.ceil(len(allranges) / amountkeys['perpage'])" as="pages" />
|
||||
<pyhp save="allranges[firstindex:lastindex]" as="displayranges" />
|
||||
|
||||
<pyhp>
|
||||
minrank = 80
|
||||
for t in displayranges:
|
||||
if t["rank"] is not None and t["rank"]+20 > minrank: minrank = t["rank"]+20
|
||||
</pyhp>
|
||||
|
||||
</pyhp>
|
||||
|
||||
<table class="list">
|
||||
<pyhp for="t" in="displayranges">
|
||||
<pyhp save="t['range']" as="thisrange" />
|
||||
<tr>
|
||||
<td><pyhp echo="thisrange.desc()" /></td>
|
||||
<td class='rank'><pyhp echo="'#' + str(t['rank']) if t['rank'] is not None else 'n/a'" /></td>
|
||||
<pyhp save="(minrank+1-t['rank'])*100/minrank if t['rank'] is not None else 0" as="prct" />
|
||||
<td class='chart'><pyhp echo="htmlgenerators.rankLink(thisrange.urikeys(),percent=prct,**filterkeys,medal=t['rank'])" /></td>
|
||||
</tr>
|
||||
</pyhp>
|
||||
</table>
|
|
@ -0,0 +1,31 @@
|
|||
<pyhp>
|
||||
if amountkeys.get("max_") is not None: amountkeys["perpage"],amountkeys["page"] = amountkeys["max_"],0
|
||||
firstindex = amountkeys["page"] * amountkeys["perpage"]
|
||||
lastindex = firstindex + amountkeys["perpage"]
|
||||
|
||||
</pyhp>
|
||||
|
||||
<pyhp import="math" />
|
||||
|
||||
<pyhp save="db.get_pulse(**limitkeys,**delimitkeys,**filterkeys)" as="allranges" />
|
||||
<pyhp save="math.ceil(len(allranges) / amountkeys['perpage'])" as="pages" />
|
||||
<pyhp save="allranges[firstindex:lastindex]" as="displayranges" />
|
||||
|
||||
<pyhp save="max(max([t['scrobbles'] for t in displayranges]),1)" as="maxbar" />
|
||||
|
||||
<table class="list">
|
||||
<pyhp for="t" in="displayranges">
|
||||
<pyhp save="t['range']" as="thisrange" />
|
||||
<tr>
|
||||
<td><pyhp echo="thisrange.desc()" /></td>
|
||||
<td class="amount"><pyhp echo="htmlgenerators.scrobblesLink(thisrange.urikeys(),amount=t['scrobbles'],**filterkeys)" /></td>
|
||||
<td class="bar"><pyhp echo="htmlgenerators.scrobblesLink(thisrange.urikeys(),percent=t['scrobbles']*100/maxbar,**filterkeys)" /></td>
|
||||
</tr>
|
||||
</pyhp>
|
||||
</table>
|
||||
|
||||
|
||||
<pyhp if="amountkeys.get('max_') is None">
|
||||
<pyhp include="pagination.pyhp" />
|
||||
<!-- <pyhp echo="htmlmodules.module_paginate(page=amountkeys['page'],pages=pages,perpage=amountkeys['perpage'],**_urikeys)" />-->
|
||||
</pyhp>
|
|
@ -1,13 +1,13 @@
|
|||
import urllib
|
||||
import database
|
||||
from .. import database
|
||||
|
||||
|
||||
def instructions(keys):
|
||||
from utilities import getArtistImage, getTrackImage
|
||||
from htmlgenerators import artistLink, artistLinks, trackLink, scrobblesLink
|
||||
from urihandler import compose_querystring, uri_to_internal, internal_to_uri
|
||||
from htmlmodules import module_performance, module_filterselection
|
||||
from malojatime import range_desc, delimit_desc
|
||||
from ..utilities import getArtistImage, getTrackImage
|
||||
from ..htmlgenerators import artistLink, artistLinks, trackLink, scrobblesLink
|
||||
from ..urihandler import compose_querystring, uri_to_internal, internal_to_uri
|
||||
from ..htmlmodules import module_performance, module_filterselection
|
||||
from ..malojatime import range_desc, delimit_desc
|
||||
|
||||
filterkeys, timekeys, delimitkeys, paginatekeys = uri_to_internal(keys)
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Proxyscrobble</title>
|
||||
<script src="javascript/cookies.js"></script>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load",function(){
|
|
@ -3,8 +3,8 @@ import urllib.request
|
|||
import hashlib
|
||||
import xml.etree.ElementTree as ET
|
||||
from bottle import redirect, request
|
||||
from database import checkAPIkey
|
||||
from external import lfmbuild
|
||||
from ..database import checkAPIkey
|
||||
from ..external import lfmbuild
|
||||
|
||||
def instructions(keys):
|
||||
authenticated = False
|