Merge branch 'master' into thirdparty_rework

This commit is contained in:
Krateng 2020-07-25 05:45:23 +02:00
commit a097d34f10
31 changed files with 629 additions and 375 deletions

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM python:3-alpine
WORKDIR /usr/src/app
RUN apk add --no-cache --virtual .build-deps \
gcc \
libxml2-dev \
libxslt-dev \
py3-pip \
libc-dev \
linux-headers \
&& \
pip3 install psutil && \
pip3 install malojaserver && \
apk del .build-deps
EXPOSE 42010
ENTRYPOINT maloja run

View File

@ -15,8 +15,10 @@ You can check [my own Maloja page](https://maloja.krateng.ch) to see what it loo
## Table of Contents
* [Why not Last.fm / Libre.fm / GNU FM?](#why-not-lastfm--librefm--gnu-fm)
* [How to install](#how-to-install)
* [Environment](#environment)
* [New Installation](#new-installation)
* [Update](#update)
* [Docker](#docker)
* [How to use](#how-to-use)
* [Basic control](#basic-control)
* [Data](#data)
@ -40,6 +42,10 @@ Also neat: You can use your **custom artist or track images**.
## How to install
### Environment
I can support you with issues best if you use **Alpine Linux**. In my experience, **2-4 GB RAM** should do nicely.
### New Installation
1) Make sure you have Python 3.5 or higher installed. You also need some basic packages that should be present on most systems, but I've provided simple shell scripts for Alpine and Ubuntu to get everything you need.
@ -53,8 +59,8 @@ Also neat: You can use your **custom artist or track images**.
5) (Recommended) Until I have a proper service implemented, I would recommend setting two cronjobs for maloja:
```
@reboot maloja start
42 0 * * * maloja restart
@reboot sleep 15 && maloja start
42 0 * * 2 maloja restart
```
@ -64,6 +70,11 @@ Also neat: You can use your **custom artist or track images**.
* Otherwise, simply run the command `maloja update` or use `pip`s update mechanic.
### Docker
There is a Dockerfile in the repo that should work by itself. You can also use the unofficial [Dockerhub repository](https://hub.docker.com/r/foxxmd/maloja) kindly provided by FoxxMD.
## How to use
### Basic control

View File

@ -1,3 +1,4 @@
#!/usr/bin/env bash
apk add python3 python3-dev gcc libxml2-dev libxslt-dev py3-pip libc-dev
apk add python3 python3-dev gcc libxml2-dev libxslt-dev py3-pip libc-dev linux-headers
pip3 install psutil
pip3 install malojaserver

View File

@ -1,4 +1,5 @@
#!/usr/bin/env bash
apt update
apt install python3 python3-pip
pip3 install psutil
pip3 install malojaserver

View File

@ -5,7 +5,7 @@ author = {
"email":"maloja@krateng.dev",
"github": "krateng"
}
version = 2,3,8
version = 2,5,3
versionstr = ".".join(str(n) for n in version)
links = {
"pypi":"malojaserver",
@ -15,21 +15,25 @@ links = {
requires = [
"bottle>=0.12.16",
"waitress>=1.3",
"doreah>=1.5.6",
"doreah>=1.6.7",
"nimrodel>=0.6.3",
"setproctitle>=1.1.10",
"wand>=0.5.4",
"lesscpy>=0.13",
"jinja2>2.11"
"jinja2>2.11",
"lru-dict>=1.1.6"
]
resources = [
"web/*/*/*",
"web/*/*",
"web/*",
"static/*/*",
"data_files/*/*",
"data_files/*/*/*"
"data_files/*/*/*",
"proccontrol/*",
"proccontrol/*/*"
]
commands = {
"maloja":"controller:main"
"maloja":"proccontrol.control:main"
}

View File

@ -1,202 +0,0 @@
#!/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, ask, prompt
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"
}
SKIP = settings.get_settings("SKIP_SETUP")
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 = prompt("",types=(str,),default=None,skip=SKIP)
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:
answer = ask("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.",default=True,skip=SKIP)
if answer:
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")
else:
pass
if settings.get_settings("NAME") is None:
name = prompt("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.",default="Generic Maloja User",skip=SKIP)
settings.update_settings(datadir("settings/settings.ini"),{"NAME":name},create_new=True)
if settings.get_settings("SEND_STATS") is None:
answer = ask("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)?",default=True,skip=SKIP)
if answer:
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!"))
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)
# return True
# else:
# print("Server is not running")
# return False
pid = getInstance()
if pid is not None:
# print("Server is not running")
# return False
# pass
# else:
os.kill(pid,signal.SIGTERM)
# print("Maloja stopped! PID: " + str(pid))
if pid is not None or pid_sv is not None:
return True
else:
return False
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():
setup()
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,
"run":direct
}
if action in actions: actions[action](*args,**kwargs)
else: print("Valid commands: " + " ".join(a for a in actions))
return True
#if __name__ == "__main__":
# main()

View File

@ -159,6 +159,9 @@ replacetitle 벌써 12시 Gotta Go Gotta Go
# ITZY
replacetitle 달라달라 (DALLA DALLA) Dalla Dalla
# K/DA
belongtogether K/DA
# Popular Remixes
artistintitle Areia Remix Areia

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

View File

@ -25,7 +25,11 @@ TRACK_SEARCH_PROVIDER = None
[Database]
DB_CACHE_SIZE = 8192 # how many MB on disk each database cache should have available.
USE_DB_CACHE = yes
CACHE_DATABASE_SHORT = true
CACHE_DATABASE_PERM = true #more permanent cache for old timeranges
DB_CACHE_ENTRIES = 10000 #experiment with this depending on your RAM
DB_MAX_MEMORY = 75 # percentage of RAM utilization (whole container, not just maloja) that should trigger a flush
INVALID_ARTISTS = ["[Unknown Artist]","Unknown Artist","Spotify"]
REMOVE_FROM_TITLE = ["(Original Mix)","(Radio Edit)","(Album Version)","(Explicit Version)","(Bonus Track)"]
USE_PARSE_PLUGINS = no
@ -63,6 +67,5 @@ EXPERIMENTAL_FEATURES = no
USE_PYHP = no #not recommended at the moment
USE_JINJA = no #overwrites pyhp preference
FEDERATION = yes #does nothing yet
UPDATE_AFTER_CRASH = no #update when server is automatically restarted
DAILY_RESTART = 2 # hour of day. no / none means no daily restarts
SKIP_SETUP = no
LOGGING = true

View File

@ -1,5 +1,6 @@
# server
from bottle import request, response, FormsDict
# rest of the project
from .cleanup import CleanerAgent, CollectorAgent
from . import utilities
@ -12,6 +13,7 @@ from .thirdparty import proxy_scrobble_all
from .__pkginfo__ import version
from .globalconf import datadir
# doreah toolkit
from doreah.logging import log
from doreah import tsv
@ -21,9 +23,11 @@ try:
from doreah.persistence import DiskDict
except: pass
import doreah
# nimrodel API
from nimrodel import EAPI as API
from nimrodel import Multi
# technical
import os
import datetime
@ -32,6 +36,8 @@ import unicodedata
from collections import namedtuple
from threading import Lock
import yaml
import lru
# url handling
from importlib.machinery import SourceFileLoader
import urllib
@ -722,7 +728,7 @@ def abouttoshutdown():
def newrule(**keys):
apikey = keys.pop("key",None)
if (checkAPIkey(apikey)):
tsv.add_entry("rules/webmade.tsv",[k for k in keys])
tsv.add_entry(datadir("rules/webmade.tsv"),[k for k in keys])
#addEntry("rules/webmade.tsv",[k for k in keys])
global db_rulestate
db_rulestate = False
@ -851,7 +857,7 @@ def rebuild(**keys):
global db_rulestate
db_rulestate = False
sync()
from .fixexisting import fix
from .proccontrol.tasks.fixexisting import fix
fix()
global cla, coa
cla = CleanerAgent()
@ -930,6 +936,7 @@ def build_db():
log("Building database...")
global SCROBBLES, ARTISTS, TRACKS
global TRACKS_NORMALIZED_SET, TRACKS_NORMALIZED, ARTISTS_NORMALIZED_SET, ARTISTS_NORMALIZED
global SCROBBLESDICT, STAMPS
SCROBBLES = []
@ -938,6 +945,11 @@ def build_db():
STAMPS = []
SCROBBLESDICT = {}
TRACKS_NORMALIZED = []
ARTISTS_NORMALIZED = []
ARTISTS_NORMALIZED_SET = set()
TRACKS_NORMALIZED_SET = set()
# parse files
db = tsv.parse_all(datadir("scrobbles"),"int","string","string",comments=False)
@ -1035,70 +1047,158 @@ def sync():
###
import copy
cache_query = {}
if doreah.version >= (0,7,1) and settings.get_settings("EXPERIMENTAL_FEATURES"):
cache_query_permanent = DiskDict(name="dbquery",folder=datadir("cache"),maxmemory=1024*1024*500,maxstorage=1024*1024*settings.get_settings("DB_CACHE_SIZE"))
if settings.get_settings("USE_DB_CACHE"):
def db_query(**kwargs):
return db_query_cached(**kwargs)
def db_aggregate(**kwargs):
return db_aggregate_cached(**kwargs)
else:
cache_query_permanent = Cache(maxmemory=1024*1024*500)
cacheday = (0,0,0)
def db_query(**kwargs):
check_cache_age()
global cache_query, cache_query_permanent
def db_query(**kwargs):
return db_query_full(**kwargs)
def db_aggregate(**kwargs):
return db_aggregate_full(**kwargs)
csz = settings.get_settings("DB_CACHE_ENTRIES")
cmp = settings.get_settings("DB_MAX_MEMORY")
try:
import psutil
use_psutil = True
except:
use_psutil = False
cache_query = lru.LRU(csz)
cache_query_perm = lru.LRU(csz)
cache_aggregate = lru.LRU(csz)
cache_aggregate_perm = lru.LRU(csz)
perm_caching = settings.get_settings("CACHE_DATABASE_PERM")
temp_caching = settings.get_settings("CACHE_DATABASE_SHORT")
cachestats = {
"cache_query":{
"hits_perm":0,
"hits_tmp":0,
"misses":0,
"objperm":cache_query_perm,
"objtmp":cache_query,
"name":"Query Cache"
},
"cache_aggregate":{
"hits_perm":0,
"hits_tmp":0,
"misses":0,
"objperm":cache_aggregate_perm,
"objtmp":cache_aggregate,
"name":"Aggregate Cache"
}
}
from doreah.regular import runhourly
@runhourly
def log_stats():
logstr = "{name}: {hitsperm} Perm Hits, {hitstmp} Tmp Hits, {misses} Misses; Current Size: {sizeperm}/{sizetmp}"
for s in (cachestats["cache_query"],cachestats["cache_aggregate"]):
log(logstr.format(name=s["name"],hitsperm=s["hits_perm"],hitstmp=s["hits_tmp"],misses=s["misses"],
sizeperm=len(s["objperm"]),sizetmp=len(s["objtmp"])),module="debug")
def db_query_cached(**kwargs):
global cache_query, cache_query_perm
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_query_permanent:
#print("Hit")
return copy.copy(cache_query_permanent.get(key))
#print("Miss")
result = db_query_full(**kwargs)
cache_query_permanent.add(key,copy.copy(result))
#print(cache_query_permanent.cache)
eligible_permanent_caching = (
"timerange" in kwargs and
not kwargs["timerange"].active() and
perm_caching
)
eligible_temporary_caching = (
not eligible_permanent_caching and
temp_caching
)
# hit permanent cache for past timeranges
if eligible_permanent_caching and key in cache_query_perm:
cachestats["cache_query"]["hits_perm"] += 1
return copy.copy(cache_query_perm.get(key))
# hit short term cache
elif eligible_temporary_caching and key in cache_query:
cachestats["cache_query"]["hits_tmp"] += 1
return copy.copy(cache_query.get(key))
else:
#print("I guess they never miss huh")
if key in cache_query: return copy.copy(cache_query[key])
cachestats["cache_query"]["misses"] += 1
result = db_query_full(**kwargs)
cache_query[key] = copy.copy(result)
if eligible_permanent_caching: cache_query_perm[key] = result
elif eligible_temporary_caching: cache_query[key] = result
return result
if use_psutil:
reduce_caches_if_low_ram()
cache_aggregate = {}
if doreah.version >= (0,7,1) and settings.get_settings("EXPERIMENTAL_FEATURES"):
cache_aggregate_permanent = DiskDict(name="dbaggregate",folder="cache",maxmemory=1024*1024*500,maxstorage=1024*1024*settings.get_settings("DB_CACHE_SIZE"))
else:
cache_aggregate_permanent = Cache(maxmemory=1024*1024*500)
def db_aggregate(**kwargs):
check_cache_age()
global cache_aggregate, cache_aggregate_permanent
return result
def db_aggregate_cached(**kwargs):
global cache_aggregate, cache_aggregate_perm
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_aggregate_permanent: return copy.copy(cache_aggregate_permanent.get(key))
result = db_aggregate_full(**kwargs)
cache_aggregate_permanent.add(key,copy.copy(result))
else:
if key in cache_aggregate: return copy.copy(cache_aggregate[key])
result = db_aggregate_full(**kwargs)
cache_aggregate[key] = copy.copy(result)
return result
eligible_permanent_caching = (
"timerange" in kwargs and
not kwargs["timerange"].active() and
perm_caching
)
eligible_temporary_caching = (
not eligible_permanent_caching and
temp_caching
)
# hit permanent cache for past timeranges
if eligible_permanent_caching and key in cache_aggregate_perm:
cachestats["cache_aggregate"]["hits_perm"] += 1
return copy.copy(cache_aggregate_perm.get(key))
# hit short term cache
elif eligible_temporary_caching and key in cache_aggregate:
cachestats["cache_aggregate"]["hits_tmp"] += 1
return copy.copy(cache_aggregate.get(key))
else:
cachestats["cache_aggregate"]["misses"] += 1
result = db_aggregate_full(**kwargs)
if eligible_permanent_caching: cache_aggregate_perm[key] = result
elif eligible_temporary_caching: cache_aggregate[key] = result
if use_psutil:
reduce_caches_if_low_ram()
return result
def invalidate_caches():
global cache_query, cache_aggregate
cache_query = {}
cache_aggregate = {}
now = datetime.datetime.utcnow()
global cacheday
cacheday = (now.year,now.month,now.day)
cache_query.clear()
cache_aggregate.clear()
log("Database caches invalidated.")
def check_cache_age():
now = datetime.datetime.utcnow()
global cacheday
if cacheday != (now.year,now.month,now.day): invalidate_caches()
def reduce_caches(to=0.75):
global cache_query, cache_aggregate, cache_query_perm, cache_aggregate_perm
for c in cache_query, cache_aggregate, cache_query_perm, cache_aggregate_perm:
currentsize = len(c)
if currentsize > 100:
targetsize = max(int(currentsize * to),10)
c.set_size(targetsize)
c.set_size(csz)
def reduce_caches_if_low_ram():
ramprct = psutil.virtual_memory().percent
if ramprct > cmp:
log("{prct}% RAM usage, reducing caches!".format(prct=ramprct),module="debug")
ratio = (cmp / ramprct) ** 3
reduce_caches(to=ratio)
####
## Database queries

View File

@ -1,23 +1,34 @@
import os
from doreah.settings import get_settings
from doreah.settings import config as settingsconfig
# 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/")
# check environment variables for data directory
# otherwise, go with defaults
setting_datadir = get_settings("DATA_DIRECTORY",files=[],environ_prefix="MALOJA_")
if setting_datadir is not None and os.path.exists(setting_datadir):
DATA_DIR = setting_datadir
else:
try:
HOME_DIR = os.environ["XDG_DATA_HOME"].split(":")[0]
assert os.path.exists(HOME_DIR)
except:
HOME_DIR = os.path.join(os.environ["HOME"],".local/share/")
DATA_DIR = os.path.join(HOME_DIR,"maloja")
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
@ -26,9 +37,6 @@ config(
pyhp={
"version": 2
},
logging={
"logfolder": datadir("logs")
},
settings={
"files":[
datadir("settings/default.ini"),
@ -44,9 +52,18 @@ config(
}
)
# because we loaded a doreah module already before setting the config, we need to to that manually
settingsconfig._readpreconfig()
config(
logging={
"logfolder": datadir("logs") if get_settings("LOGGING") else None
}
)
settingsconfig._readpreconfig()
from doreah.settings import get_settings
# thumbor

View File

View File

@ -0,0 +1,94 @@
import subprocess
from doreah import settings
from doreah.control import mainfunction
from doreah.io import col
import os
import signal
from .setup import setup
from . import tasks
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 restart():
stop()
start()
def start():
if getInstanceSupervisor() is not None:
print("Maloja is already running.")
else:
setup()
try:
#p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
sp = subprocess.Popen(["python3","-m","maloja.proccontrol.supervisor"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
print(col["green"]("Maloja started!"))
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 stop():
pid_sv = getInstanceSupervisor()
if pid_sv is not None:
os.kill(pid_sv,signal.SIGTERM)
pid = getInstance()
if pid is not None:
os.kill(pid,signal.SIGTERM)
if pid is not None or pid_sv is not None:
print("Maloja stopped!")
return True
else:
return False
def direct():
setup()
from .. import server
@mainfunction({"l":"level"},shield=True)
def main(action,*args,**kwargs):
actions = {
"start":start,
"restart":restart,
"stop":stop,
"run":direct,
"debug":direct,
"import":tasks.loadlastfm,
"backup":tasks.backuphere,
# "update":update,
"fix":tasks.fixdb
}
if action in actions: actions[action](*args,**kwargs)
else: print("Valid commands: " + " ".join(a for a in actions))
return True

View File

@ -0,0 +1,71 @@
import pkg_resources
from distutils import dir_util
from doreah import settings
from doreah.io import col, ask, prompt
import os
from ..globalconf import datadir
# 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"
}
def copy_initial_local_files():
folder = pkg_resources.resource_filename("maloja","data_files")
#shutil.copy(folder,DATA_DIR)
dir_util.copy_tree(folder,datadir(),update=False)
def setup():
copy_initial_local_files()
SKIP = settings.get_settings("SKIP_SETUP")
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 = prompt("",types=(str,),default=None,skip=SKIP)
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:
answer = ask("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.",default=True,skip=SKIP)
if answer:
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")
else:
pass
if settings.get_settings("NAME") is None:
name = prompt("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.",default="Generic Maloja User",skip=SKIP)
settings.update_settings(datadir("settings/settings.ini"),{"NAME":name},create_new=True)
if settings.get_settings("SEND_STATS") is None:
answer = ask("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)?",default=True,skip=SKIP)
if answer:
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)

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
import os
import subprocess
import setproctitle
import signal
from doreah.logging import log
from doreah.settings import get_settings
from .control import getInstance
setproctitle.setproctitle("maloja_supervisor")
def update():
log("Updating...",module="supervisor")
try:
os.system("pip3 install maloja --upgrade --no-cache-dir")
except:
log("Could not update.",module="supervisor")
def start():
try:
p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
return p
except e:
log("Error starting Maloja: " + str(e),module="supervisor")
while True:
log("Maloja is not running, starting...",module="supervisor")
if get_settings("UPDATE_AFTER_CRASH"):
update()
process = start()
process.wait()

View File

@ -0,0 +1,36 @@
import os
from doreah.io import ask
from ...globalconf import datadir
def loadlastfm(filename):
if not os.path.exists(filename):
print("File could not be found.")
return
if os.path.exists(datadir("scrobbles/lastfmimport.tsv")):
overwrite = ask("Already imported Last.FM data. Overwrite?",default=False)
if not overwrite: 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 backuphere():
from .backup import backup
backup(folder=os.getcwd())
def update():
os.system("pip3 install malojaserver --upgrade --no-cache-dir")
from ..control import restart
restart()
def fixdb():
from .fixexisting import fix
fix()

View File

@ -2,7 +2,10 @@ import tarfile
from datetime import datetime
import glob
import os
from .globalconf import datadir
from ...globalconf import datadir
from pathlib import PurePath
from doreah.logging import log
user_files = {
@ -25,6 +28,8 @@ def backup(folder,level="full"):
for g in selected_files:
real_files += glob.glob(datadir(g))
log("Creating backup of " + str(len(real_files)) + " files...")
now = datetime.utcnow()
timestr = now.strftime("%Y_%m_%d_%H_%M_%S")
filename = "maloja_backup_" + timestr + ".tar.gz"
@ -32,4 +37,7 @@ def backup(folder,level="full"):
assert not os.path.exists(archivefile)
with tarfile.open(name=archivefile,mode="x:gz") as archive:
for f in real_files:
archive.add(f)
p = PurePath(f)
r = p.relative_to(datadir())
archive.add(f,arcname=r)
log("Backup created!")

View File

@ -1,7 +1,7 @@
import os
from .globalconf import datadir
from ...globalconf import datadir
import re
from .cleanup import CleanerAgent
from ...cleanup import CleanerAgent
from doreah.logging import log
import difflib
import datetime
@ -35,9 +35,10 @@ def fix():
#with open(datadir("logs","dbfix",nowstr + ".log"),"a") as logfile:
log("Fixing database...")
for filename in os.listdir(datadir("scrobbles")):
if filename.endswith(".tsv"):
log("Fix file " + filename)
filename_new = filename + "_new"
with open(datadir("scrobbles",filename_new),"w") as newfile:
@ -68,3 +69,5 @@ def fix():
with open(datadir("scrobbles",filename + ".rulestate"),"w") as checkfile:
checkfile.write(wendigo.checksums)
log("Database fixed!")

View File

@ -1,6 +1,6 @@
import os, datetime, re
from .cleanup import *
from .utilities import *
from ...cleanup import *
from ...utilities import *

View File

@ -245,7 +245,7 @@ def static_html(name):
template = jinjaenv.get_template(name + '.jinja')
res = template.render(**LOCAL_CONTEXT)
log("Generated page {name} in {time}s (Jinja)".format(name=name,time=clock.stop()),module="debug")
log("Generated page {name} in {time:.5f}s (Jinja)".format(name=name,time=clock.stop()),module="debug")
return res
# if a pyhp file exists, use this
@ -272,7 +272,7 @@ def static_html(name):
#response.set_header("Content-Type","application/xhtml+xml")
res = pyhpfile(pthjoin(WEBFOLDER,"pyhp",name + ".pyhp"),environ)
log("Generated page {name} in {time}s (PYHP)".format(name=name,time=clock.stop()),module="debug")
log("Generated page {name} in {time:.5f}s (PYHP)".format(name=name,time=clock.stop()),module="debug")
return res
# if not, use the old way
@ -316,7 +316,7 @@ def static_html(name):
response.set_header("Link",",".join(linkheaders))
log("Generated page " + name + " in " + str(clock.stop()) + "s (Python+HTML)",module="debug")
log("Generated page {name} in {time:.5f}s (Python+HTML)".format(name=name,time=clock.stop()),module="debug")
return html
#return static_file("web/" + name + ".html",root="")

View File

@ -291,13 +291,13 @@ span.stat_selector_pulse,span.stat_selector_topartists,span.stat_selector_toptra
h2 {
h2.headerwithextra {
display:inline-block;
padding-right:5px;
margin-bottom:10px;
margin-top:15px;
}
h2+span.afterheader {
h2.headerwithextra+span.afterheader {
color:@TEXT_COLOR_TERTIARY;
}

View File

@ -1,67 +0,0 @@
#!/usr/bin/env python3
import os
import subprocess
import time
import setproctitle
import signal
from datetime import datetime
from doreah.logging import log
from doreah.settings import get_settings
setproctitle.setproctitle("maloja_supervisor")
lastrestart = ()
def get_pid():
try:
output = subprocess.check_output(["pidof","Maloja"])
return int(output)
except:
return None
def update():
log("Updating...",module="supervisor")
try:
os.system("pip3 install maloja --upgrade --no-cache-dir")
except:
log("Could not update.",module="supervisor")
def start():
try:
p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
except e:
log("Error starting Maloja: " + str(e),module="supervisor")
while True:
now = datetime.now()
today = now.year, now.month, now.day
pid = get_pid()
if pid:
restart = get_settings("DAILY_RESTART")
if restart not in [None,False]:
if today != lastrestart:
if now.hour == restart:
log("Daily restart...",module="supervisor")
os.kill(pid,signal.SIGTERM)
start()
lastrestart = today
else:
log("Maloja is not running, starting...",module="supervisor")
if get_settings("UPDATE_AFTER_CRASH"):
update()
start()
lastrestart = today
time.sleep(60)

View File

@ -27,13 +27,16 @@ from .globalconf import datadir
def serialize(obj):
try:
return json.dumps(obj)
return serialize(obj.hashable())
except:
if isinstance(obj,list) or isinstance(obj,tuple):
return "[" + ",".join(serialize(o) for o in obj) + "]"
elif isinstance(obj,dict):
return "{" + ",".join(serialize(o) + ":" + serialize(obj[o]) for o in obj) + "}"
return json.dumps(obj.hashable())
try:
return json.dumps(obj)
except:
if isinstance(obj,list) or isinstance(obj,tuple):
return "[" + ",".join(serialize(o) for o in obj) + "]"
elif isinstance(obj,dict):
return "{" + ",".join(serialize(o) + ":" + serialize(obj[o]) for o in obj) + "}"
return json.dumps(obj.hashable())
#if isinstance(obj,list) or if isinstance(obj,tuple):

View File

@ -45,7 +45,7 @@
{% endif %}
</td>
<td class="text">
<h1>{{ artist }}</h1>
<h1 class="headerwithextra">{{ artist }}</h1>
{% if competes %}<span class="rank"><a href="/charts_artists?max=100">#{{ info.position }}</a></span>{% endif %}
<br/>
{% if competes and included %}
@ -83,7 +83,7 @@
<tr>
<td>
<h2><a href='/pulse?{{ encodedartist }}&amp;step=year&amp;trail=1'>Pulse</a></h2>
<h2 class="headerwithextra"><a href='/pulse?{{ encodedartist }}&amp;step=year&amp;trail=1'>Pulse</a></h2>
<br/>
{% for range in xranges %}
<span
@ -112,7 +112,7 @@
</td>
<td>
<!-- We use the same classes / function calls here because we want it to switch together with pulse -->
<h2><a href='/performance?{{ encodedcredited }}&amp;step=year&amp;trail=1'>Performance</a></h2>
<h2 class="headerwithextra"><a href='/performance?{{ encodedcredited }}&amp;step=year&amp;trail=1'>Performance</a></h2>
{% if not competes %}<span class="afterheader">of {{ htmlgenerators.artistLink(credited) }}</span>
{% endif %}
<br/>

View File

@ -0,0 +1,34 @@
{% extends "base.jinja" %}
{% block title %}Maloja - {{ artist }}{% endblock %}
{% block scripts %}
<script src="/datechange.js" async></script>
{% endblock %}
{% set charts = dbp.get_charts_artists(filterkeys,limitkeys) %}
{% block content %}
<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>{{ limitkeys.timerange.desc(prefix=True) }}</span>
<br/><br/>
{{ htmlmodules.module_filterselection(_urikeys) }}
</td>
</tr>
</table>
{% import 'partials/charts_artists.jinja' as charts_artists %}
{{ charts_artists.charts_artists(limitkeys,amountkeys,compare=False) }}
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "base.jinja" %}
{% block title %}Maloja - Track Charts{% endblock %}
{% block scripts %}
<script src="/datechange.js" async></script>
{% endblock %}
{% set charts = dbp.get_charts_tracks(filterkeys,limitkeys) %}
{% block content %}
<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>{{ limitkeys.timerange.desc(prefix=True) }}</span>
<br/><br/>
{{ htmlmodules.module_filterselection(_urikeys) }}
</td>
</tr>
</table>
{% import 'partials/charts_tracks.jinja' as charts_tracks %}
{{ charts_tracks.charts_tracks(filterkeys,limitkeys,amountkeys,charts=charts,compare=false) }}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% macro charts_artists(limitkeys,amountkeys,charts=None,compare=False) %}
{% if charts is none %}
{% set charts = dbp.get_charts_artists(limitkeys) %}
{% endif %}
{% if compare %}
{% endif %}
{% set firstindex = amountkeys.page * amountkeys.perpage %}
{% set lastindex = firstindex + amountkeys.perpage %}
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
<table class='list'>
{% for e in charts %}
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
<tr>
<!-- Rank -->
<td class="rank">{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}</td>
<!-- Rank change -->
{% if false %}
{% if e not in prevcharts %}<td class='rankup' title='New'>🆕</td>{% endif %}
{% endif %}
<!-- artist -->
{{ htmlgenerators.entity_column(e['artist']) }}
<!-- scrobbles -->
<td class="amount">{{ htmlgenerators.scrobblesArtistLink(e['artist'],urihandler.internal_to_uri(limitkeys),amount=e['scrobbles']) }}</td>
<td class="bar">{{ htmlgenerators.scrobblesArtistLink(e['artist'],urihandler.internal_to_uri(limitkeys),percent=e['scrobbles']*100/maxbar) }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
{%- endmacro %}

View File

@ -1,7 +1,8 @@
{% macro charts_tracks(filterkeys,limitkeys,amountkeys,compare=False) %}
{% macro charts_tracks(filterkeys,limitkeys,amountkeys,charts=None,compare=False) %}
{% set tracks = dbp.get_charts_tracks(filterkeys,limitkeys) %}
{% if charts is none %}
{% set charts = dbp.get_charts_tracks(filterkeys,limitkeys) %}
{% endif %}
{% if compare %}
{% if compare is true %}
{% set compare = limitkeys.timerange.next(step=-1) %}
@ -9,11 +10,11 @@
{% set prevtracks = dbp.get_charts_tracks(filterkeys,{'timerange':compare}) %}
{% set lastrank = {} %}
{% for t in tracks %}
{% for t in charts %}
{% if lastrank.update({(t.track.artists,t.track.title):t.rank}) %}{% endif %}
{% endfor %}
{% for t in tracks %}
{% for t in charts %}
{% if (t.track.artists,t.track.title) in lastrank %}
{% if t.update({'lastrank':lastrank[(t.track.artists,t.track.title)]}) %}{% endif %}
{% endif %}
@ -24,10 +25,10 @@
{% set lastindex = firstindex + amountkeys.perpage %}
{% set maxbar = tracks[0]['scrobbles'] if tracks != [] else 0 %}
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
<table class='list'>
{% for e in tracks %}
{% for e in charts %}
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
<tr>
<!-- Rank -->

View File

@ -40,7 +40,7 @@
</td>
<td class="text">
<span>{{ htmlgenerators.artistLinks(track.artists) }}</span><br/>
<h1>{{ track.title }}</h1>
<h1 class="headerwithextra">{{ track.title }}</h1>
{{ awards.certs(track) }}
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
<br/>

View File

@ -112,7 +112,7 @@
<h2>External</h2>
<a class="textlink" href="https://github.com/krateng/maloja/issues/new">Report Issue</a><br/>
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://github.com/krateng/maloja/issues/new">Report Issue</a><br/>
<pyhp include="common/footer.html" />
</body>

View File

@ -1,8 +1,10 @@
bottle>=0.12.16
waitress>=1.3
doreah>=1.2.7
nimrodel>=0.4.9
doreah>=1.6.7
nimrodel>=0.6.3
setproctitle>=1.1.10
wand>=0.5.4
lesscpy>=0.13
pip>=19.3
jinja2>2.11
lru-dict>=1.1.6

View File

@ -6,7 +6,7 @@ maloja_scrobbler_selector_metadata = ".//div[@class='now-playing-bar__left']"
maloja_scrobbler_selector_title = ".//a[@data-testid='nowplaying-track-link']/text()"
maloja_scrobbler_selector_artists = ".//a[contains(@href,'/artist/')]"
maloja_scrobbler_selector_artist = "./text()"
maloja_scrobbler_selector_duration = ".//div[@class='playback-bar__progress-time'][2]/text()"
maloja_scrobbler_selector_duration = ".//div[@class='playback-bar']/div[3]/text()"
maloja_scrobbler_selector_control = ".//div[contains(@class,'player-controls__buttons')]/div[3]/button/@title"