malojaserver 3.2.2__py3-none-any.whl → 3.2.3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maloja/__main__.py +1 -1
- maloja/__pkginfo__.py +1 -1
- maloja/apis/_base.py +26 -19
- maloja/apis/_exceptions.py +1 -1
- maloja/apis/audioscrobbler.py +35 -7
- maloja/apis/audioscrobbler_legacy.py +5 -5
- maloja/apis/listenbrainz.py +7 -5
- maloja/apis/native_v1.py +43 -26
- maloja/cleanup.py +9 -7
- maloja/data_files/config/rules/predefined/krateng_kpopgirlgroups.tsv +2 -2
- maloja/database/__init__.py +55 -23
- maloja/database/associated.py +10 -6
- maloja/database/exceptions.py +28 -3
- maloja/database/sqldb.py +216 -168
- maloja/dev/profiler.py +3 -4
- maloja/images.py +6 -0
- maloja/malojauri.py +2 -0
- maloja/pkg_global/conf.py +29 -28
- maloja/proccontrol/tasks/export.py +2 -1
- maloja/proccontrol/tasks/import_scrobbles.py +57 -15
- maloja/server.py +4 -5
- maloja/setup.py +13 -7
- maloja/web/jinja/abstracts/base.jinja +1 -1
- maloja/web/jinja/admin_albumless.jinja +2 -0
- maloja/web/jinja/admin_overview.jinja +3 -3
- maloja/web/jinja/admin_setup.jinja +1 -1
- maloja/web/jinja/partials/album_showcase.jinja +1 -1
- maloja/web/jinja/snippets/entityrow.jinja +2 -2
- maloja/web/jinja/snippets/links.jinja +3 -1
- maloja/web/static/css/maloja.css +8 -2
- maloja/web/static/css/startpage.css +2 -2
- maloja/web/static/js/manualscrobble.js +1 -1
- maloja/web/static/js/notifications.js +16 -8
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/RECORD +38 -38
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/entry_points.txt +0 -0
maloja/dev/profiler.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
import cProfile, pstats
|
4
|
+
import time
|
4
5
|
|
5
6
|
from doreah.logging import log
|
6
|
-
from doreah.timing import Clock
|
7
7
|
|
8
8
|
from ..pkg_global.conf import data_dir
|
9
9
|
|
@@ -27,8 +27,7 @@ def profile(func):
|
|
27
27
|
|
28
28
|
def newfunc(*args,**kwargs):
|
29
29
|
|
30
|
-
|
31
|
-
clock.start()
|
30
|
+
starttime = time.time()
|
32
31
|
|
33
32
|
if FULL_PROFILE:
|
34
33
|
benchmarkfolder = data_dir['logs']("benchmarks")
|
@@ -44,7 +43,7 @@ def profile(func):
|
|
44
43
|
if FULL_PROFILE:
|
45
44
|
localprofiler.disable()
|
46
45
|
|
47
|
-
seconds =
|
46
|
+
seconds = time.time() - starttime
|
48
47
|
|
49
48
|
if not SINGLE_CALLS:
|
50
49
|
times.setdefault(realfunc,[]).append(seconds)
|
maloja/images.py
CHANGED
@@ -284,6 +284,12 @@ def image_request(artist_id=None,track_id=None,album_id=None):
|
|
284
284
|
if result is not None:
|
285
285
|
# we got an entry, even if it's that there is no image (value None)
|
286
286
|
if result['value'] is None:
|
287
|
+
# fallback to album regardless of setting (because we have no image)
|
288
|
+
if track_id:
|
289
|
+
track = database.sqldb.get_track(track_id)
|
290
|
+
if track.get("album"):
|
291
|
+
album_id = database.sqldb.get_album_id(track["album"])
|
292
|
+
return image_request(album_id=album_id)
|
287
293
|
# use placeholder
|
288
294
|
if malojaconfig["FANCY_PLACEHOLDER_ART"]:
|
289
295
|
placeholder_url = "https://generative-placeholders.glitch.me/image?width=300&height=300&style="
|
maloja/malojauri.py
CHANGED
@@ -29,6 +29,8 @@ def uri_to_internal(keys,accepted_entities=('artist','track','album'),forceTrack
|
|
29
29
|
|
30
30
|
# 1
|
31
31
|
filterkeys = {}
|
32
|
+
# this only takes care of the logic - what kind of entity we're dealing with
|
33
|
+
# it does not check with the database if it exists or what the canonical name is!!!
|
32
34
|
if "track" in accepted_entities and "title" in keys:
|
33
35
|
filterkeys.update({"track":{"artists":keys.getall("trackartist"),"title":keys.get("title")}})
|
34
36
|
if "artist" in accepted_entities and "artist" in keys:
|
maloja/pkg_global/conf.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
import os
|
2
|
+
|
3
|
+
import doreah.auth
|
4
|
+
import doreah.logging
|
2
5
|
from doreah.configuration import Configuration
|
3
6
|
from doreah.configuration import types as tp
|
4
7
|
|
@@ -29,9 +32,7 @@ pthj = os.path.join
|
|
29
32
|
def is_dir_usable(pth):
|
30
33
|
try:
|
31
34
|
os.makedirs(pth,exist_ok=True)
|
32
|
-
os.
|
33
|
-
os.remove(pthj(pth,".test"))
|
34
|
-
return True
|
35
|
+
return os.access(pth,os.W_OK)
|
35
36
|
except Exception:
|
36
37
|
return False
|
37
38
|
|
@@ -179,7 +180,7 @@ malojaconfig = Configuration(
|
|
179
180
|
"name":(tp.String(), "Name", "Generic Maloja User")
|
180
181
|
},
|
181
182
|
"Third Party Services":{
|
182
|
-
"metadata_providers":(tp.List(tp.String()), "Metadata Providers", ['lastfm','spotify','deezer','audiodb','musicbrainz'], "
|
183
|
+
"metadata_providers":(tp.List(tp.String()), "Metadata Providers", ['lastfm','spotify','deezer','audiodb','musicbrainz'], "List of which metadata providers should be used in what order. Musicbrainz is rate-limited and should not be used first."),
|
183
184
|
"scrobble_lastfm":(tp.Boolean(), "Proxy-Scrobble to Last.fm", False),
|
184
185
|
"lastfm_api_key":(tp.String(), "Last.fm API Key", None),
|
185
186
|
"lastfm_api_secret":(tp.String(), "Last.fm API Secret", None),
|
@@ -310,6 +311,12 @@ data_directories = {
|
|
310
311
|
}
|
311
312
|
|
312
313
|
for identifier,path in data_directories.items():
|
314
|
+
if path is None:
|
315
|
+
continue
|
316
|
+
|
317
|
+
if malojaconfig.readonly and (path == dir_settings['config'] or path.startswith(dir_settings['config']+'/')):
|
318
|
+
continue
|
319
|
+
|
313
320
|
try:
|
314
321
|
os.makedirs(path,exist_ok=True)
|
315
322
|
if not is_dir_usable(path): raise PermissionError(f"Directory {path} is not usable!")
|
@@ -320,41 +327,35 @@ for identifier,path in data_directories.items():
|
|
320
327
|
print("Cannot use",path,"for cache, finding new folder...")
|
321
328
|
data_directories['cache'] = dir_settings['cache'] = malojaconfig['DIRECTORY_CACHE'] = find_good_folder('cache')
|
322
329
|
else:
|
323
|
-
print("Directory
|
330
|
+
print(f"Directory for {identifier} ({path}) is not writeable.")
|
324
331
|
print("Please change permissions or settings!")
|
332
|
+
print("Make sure Maloja has write and execute access to this directory.")
|
325
333
|
raise
|
326
334
|
|
335
|
+
class DataDirs:
|
336
|
+
def __init__(self, dirs):
|
337
|
+
self.dirs = dirs
|
327
338
|
|
328
|
-
|
329
|
-
|
330
|
-
}
|
331
|
-
|
332
|
-
|
339
|
+
def __getitem__(self, key):
|
340
|
+
return lambda *x, k=key: pthj(self.dirs[k], *x)
|
333
341
|
|
334
|
-
|
342
|
+
data_dir = DataDirs(data_directories)
|
335
343
|
|
336
|
-
|
337
|
-
|
338
|
-
config(
|
339
|
-
auth={
|
340
|
-
"multiuser":False,
|
341
|
-
"cookieprefix":"maloja",
|
342
|
-
"stylesheets":["/maloja.css"],
|
343
|
-
"dbfile":data_dir['auth']("auth.ddb")
|
344
|
-
},
|
345
|
-
logging={
|
346
|
-
"logfolder": data_dir['logs']() if malojaconfig["LOGGING"] else None
|
347
|
-
},
|
348
|
-
regular={
|
349
|
-
"offset": malojaconfig["TIMEZONE"]
|
350
|
-
}
|
351
|
-
)
|
344
|
+
### DOREAH OBJECTS
|
352
345
|
|
346
|
+
auth = doreah.auth.AuthManager(singleuser=True,cookieprefix='maloja',stylesheets=("/maloja.css",),dbfile=data_dir['auth']("auth.sqlite"))
|
353
347
|
|
348
|
+
#logger = doreah.logging.Logger(logfolder=data_dir['logs']() if malojaconfig["LOGGING"] else None)
|
349
|
+
#log = logger.log
|
354
350
|
|
351
|
+
# this is not how its supposed to be done, but lets ease the transition
|
352
|
+
doreah.logging.defaultlogger.logfolder = data_dir['logs']() if malojaconfig["LOGGING"] else None
|
355
353
|
|
356
354
|
|
357
|
-
|
355
|
+
try:
|
356
|
+
custom_css_files = [f for f in os.listdir(data_dir['css']()) if f.lower().endswith('.css')]
|
357
|
+
except FileNotFoundError:
|
358
|
+
custom_css_files = []
|
358
359
|
|
359
360
|
from ..database.sqldb import set_maloja_info
|
360
361
|
set_maloja_info({'last_run_version':VERSION})
|
@@ -12,11 +12,12 @@ def export(targetfolder=None):
|
|
12
12
|
targetfolder = os.getcwd()
|
13
13
|
|
14
14
|
timestr = time.strftime("%Y_%m_%d_%H_%M_%S")
|
15
|
+
timestamp = int(time.time()) # ok this is technically a separate time get from above, but those ms are not gonna matter, and im too lazy to change it all to datetime
|
15
16
|
filename = f"maloja_export_{timestr}.json"
|
16
17
|
outputfile = os.path.join(targetfolder,filename)
|
17
18
|
assert not os.path.exists(outputfile)
|
18
19
|
|
19
|
-
data = {'scrobbles':get_scrobbles()}
|
20
|
+
data = {'maloja':{'export_time': timestamp },'scrobbles':get_scrobbles()}
|
20
21
|
with open(outputfile,'w') as outfd:
|
21
22
|
json.dump(data,outfd,indent=3)
|
22
23
|
|
@@ -32,37 +32,53 @@ def import_scrobbles(inputf):
|
|
32
32
|
}
|
33
33
|
|
34
34
|
filename = os.path.basename(inputf)
|
35
|
+
importfunc = None
|
35
36
|
|
36
|
-
|
37
|
-
|
37
|
+
|
38
|
+
if re.match(r".*\.csv", filename):
|
39
|
+
typeid,typedesc = "lastfm", "Last.fm (benjaminbenben export)"
|
38
40
|
importfunc = parse_lastfm
|
39
41
|
|
40
|
-
elif re.match(r"Streaming_History_Audio.+\.json",filename):
|
41
|
-
typeid,typedesc = "spotify","Spotify"
|
42
|
+
elif re.match(r"Streaming_History_Audio.+\.json", filename):
|
43
|
+
typeid,typedesc = "spotify", "Spotify"
|
42
44
|
importfunc = parse_spotify_lite
|
43
45
|
|
44
|
-
elif re.match(r"endsong_[0-9]+\.json",filename):
|
45
|
-
typeid,typedesc = "spotify","Spotify"
|
46
|
+
elif re.match(r"endsong_[0-9]+\.json", filename):
|
47
|
+
typeid,typedesc = "spotify", "Spotify"
|
46
48
|
importfunc = parse_spotify
|
47
49
|
|
48
|
-
elif re.match(r"StreamingHistory[0-9]+\.json",filename):
|
49
|
-
typeid,typedesc = "spotify","Spotify"
|
50
|
+
elif re.match(r"StreamingHistory[0-9]+\.json", filename):
|
51
|
+
typeid,typedesc = "spotify", "Spotify"
|
50
52
|
importfunc = parse_spotify_lite_legacy
|
51
53
|
|
52
|
-
elif re.match(r"maloja_export[_0-9]*\.json",filename):
|
53
|
-
typeid,typedesc = "maloja","Maloja"
|
54
|
+
elif re.match(r"maloja_export[_0-9]*\.json", filename):
|
55
|
+
typeid,typedesc = "maloja", "Maloja"
|
54
56
|
importfunc = parse_maloja
|
55
57
|
|
56
58
|
# username_lb-YYYY-MM-DD.json
|
57
|
-
elif re.match(r".*_lb-[0-9-]+\.json",filename):
|
58
|
-
typeid,typedesc = "listenbrainz","ListenBrainz"
|
59
|
+
elif re.match(r".*_lb-[0-9-]+\.json", filename):
|
60
|
+
typeid,typedesc = "listenbrainz", "ListenBrainz"
|
59
61
|
importfunc = parse_listenbrainz
|
60
62
|
|
61
|
-
elif re.match(r"\.scrobbler\.log",filename):
|
62
|
-
typeid,typedesc = "rockbox","Rockbox"
|
63
|
+
elif re.match(r"\.scrobbler\.log", filename):
|
64
|
+
typeid,typedesc = "rockbox", "Rockbox"
|
63
65
|
importfunc = parse_rockbox
|
64
66
|
|
65
|
-
|
67
|
+
elif re.match(r"recenttracks-.*\.json", filename):
|
68
|
+
typeid, typedesc = "lastfm", "Last.fm (ghan export)"
|
69
|
+
importfunc = parse_lastfm_ghan
|
70
|
+
|
71
|
+
elif re.match(r".*\.json",filename):
|
72
|
+
try:
|
73
|
+
with open(filename,'r') as fd:
|
74
|
+
data = json.load(fd)
|
75
|
+
if 'maloja' in data:
|
76
|
+
typeid,typedesc = "maloja","Maloja"
|
77
|
+
importfunc = parse_maloja
|
78
|
+
except Exception:
|
79
|
+
pass
|
80
|
+
|
81
|
+
if not importfunc:
|
66
82
|
print("File",inputf,"could not be identified as a valid import source.")
|
67
83
|
return result
|
68
84
|
|
@@ -131,6 +147,7 @@ def import_scrobbles(inputf):
|
|
131
147
|
|
132
148
|
return result
|
133
149
|
|
150
|
+
|
134
151
|
def parse_spotify_lite_legacy(inputf):
|
135
152
|
pth = os.path
|
136
153
|
# use absolute paths internally for peace of mind. just change representation for console output
|
@@ -243,6 +260,7 @@ def parse_spotify_lite(inputf):
|
|
243
260
|
|
244
261
|
print()
|
245
262
|
|
263
|
+
|
246
264
|
def parse_spotify(inputf):
|
247
265
|
pth = os.path
|
248
266
|
# use absolute paths internally for peace of mind. just change representation for console output
|
@@ -354,6 +372,7 @@ def parse_spotify(inputf):
|
|
354
372
|
|
355
373
|
print()
|
356
374
|
|
375
|
+
|
357
376
|
def parse_lastfm(inputf):
|
358
377
|
|
359
378
|
with open(inputf,'r',newline='') as inputfd:
|
@@ -388,6 +407,29 @@ def parse_lastfm(inputf):
|
|
388
407
|
yield ('FAIL',None,f"{row} (Line {line}) could not be parsed. Scrobble not imported. ({repr(e)})")
|
389
408
|
continue
|
390
409
|
|
410
|
+
|
411
|
+
def parse_lastfm_ghan(inputf):
|
412
|
+
with open(inputf, 'r') as inputfd:
|
413
|
+
data = json.load(inputfd)
|
414
|
+
|
415
|
+
skip = 50000
|
416
|
+
for entry in data:
|
417
|
+
for track in entry['track']:
|
418
|
+
skip -= 1
|
419
|
+
#if skip: continue
|
420
|
+
#print(track)
|
421
|
+
#input()
|
422
|
+
|
423
|
+
yield ('CONFIDENT_IMPORT', {
|
424
|
+
'track_title': track['name'],
|
425
|
+
'track_artists': track['artist']['#text'],
|
426
|
+
'track_length': None,
|
427
|
+
'album_name': track['album']['#text'],
|
428
|
+
'scrobble_time': int(track['date']['uts']),
|
429
|
+
'scrobble_duration': None
|
430
|
+
}, '')
|
431
|
+
|
432
|
+
|
391
433
|
def parse_listenbrainz(inputf):
|
392
434
|
|
393
435
|
with open(inputf,'r') as inputfd:
|
maloja/server.py
CHANGED
@@ -12,14 +12,13 @@ from jinja2.exceptions import TemplateNotFound
|
|
12
12
|
|
13
13
|
# doreah toolkit
|
14
14
|
from doreah.logging import log
|
15
|
-
from doreah import auth
|
16
15
|
|
17
16
|
# rest of the project
|
18
17
|
from . import database
|
19
18
|
from .database.jinjaview import JinjaDBConnection
|
20
19
|
from .images import image_request
|
21
20
|
from .malojauri import uri_to_internal, remove_identical
|
22
|
-
from .pkg_global.conf import malojaconfig, data_dir
|
21
|
+
from .pkg_global.conf import malojaconfig, data_dir, auth
|
23
22
|
from .pkg_global import conf
|
24
23
|
from .jinjaenv.context import jinja_environment
|
25
24
|
from .apis import init_apis, apikeystore
|
@@ -97,7 +96,7 @@ aliases = {
|
|
97
96
|
|
98
97
|
### API
|
99
98
|
|
100
|
-
auth.authapi.mount(server=webserver)
|
99
|
+
conf.auth.authapi.mount(server=webserver)
|
101
100
|
init_apis(webserver)
|
102
101
|
|
103
102
|
# redirects for backwards compatibility
|
@@ -197,7 +196,7 @@ def jinja_page(name):
|
|
197
196
|
if name in aliases: redirect(aliases[name])
|
198
197
|
keys = remove_identical(FormsDict.decode(request.query))
|
199
198
|
|
200
|
-
adminmode = request.cookies.get("adminmode") == "true" and auth.
|
199
|
+
adminmode = request.cookies.get("adminmode") == "true" and auth.check_request(request)
|
201
200
|
|
202
201
|
with JinjaDBConnection() as conn:
|
203
202
|
|
@@ -222,7 +221,7 @@ def jinja_page(name):
|
|
222
221
|
return res
|
223
222
|
|
224
223
|
@webserver.route("/<name:re:admin.*>")
|
225
|
-
@auth.
|
224
|
+
@auth.authenticated_function()
|
226
225
|
def jinja_page_private(name):
|
227
226
|
return jinja_page(name)
|
228
227
|
|
maloja/setup.py
CHANGED
@@ -6,9 +6,8 @@ try:
|
|
6
6
|
except ImportError:
|
7
7
|
import distutils
|
8
8
|
from doreah.io import col, ask, prompt
|
9
|
-
from doreah import auth
|
10
9
|
|
11
|
-
from .pkg_global.conf import data_dir, dir_settings, malojaconfig
|
10
|
+
from .pkg_global.conf import data_dir, dir_settings, malojaconfig, auth
|
12
11
|
|
13
12
|
|
14
13
|
|
@@ -25,6 +24,12 @@ ext_apikeys = [
|
|
25
24
|
def copy_initial_local_files():
|
26
25
|
with resources.files("maloja") / 'data_files' as folder:
|
27
26
|
for cat in dir_settings:
|
27
|
+
if dir_settings[cat] is None:
|
28
|
+
continue
|
29
|
+
|
30
|
+
if cat == 'config' and malojaconfig.readonly:
|
31
|
+
continue
|
32
|
+
|
28
33
|
distutils.dir_util.copy_tree(os.path.join(folder,cat),dir_settings[cat],update=False)
|
29
34
|
|
30
35
|
charset = list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
@@ -38,7 +43,6 @@ def setup():
|
|
38
43
|
SKIP = malojaconfig["SKIP_SETUP"]
|
39
44
|
|
40
45
|
try:
|
41
|
-
|
42
46
|
print("Various external services can be used to display images. If not enough of them are set up, only local images will be used.")
|
43
47
|
for k in ext_apikeys:
|
44
48
|
keyname = malojaconfig.get_setting_info(k)['name']
|
@@ -46,6 +50,8 @@ def setup():
|
|
46
50
|
if key is False:
|
47
51
|
print(f"\tCurrently not using a {col['red'](keyname)} for image display.")
|
48
52
|
elif key is None or key == "ASK":
|
53
|
+
if malojaconfig.readonly:
|
54
|
+
continue
|
49
55
|
promptmsg = f"\tPlease enter your {col['gold'](keyname)}. If you do not want to use one at this moment, simply leave this empty and press Enter."
|
50
56
|
key = prompt(promptmsg,types=(str,),default=False,skip=SKIP)
|
51
57
|
malojaconfig[k] = key
|
@@ -67,10 +73,10 @@ def setup():
|
|
67
73
|
|
68
74
|
if forcepassword is not None:
|
69
75
|
# user has specified to force the pw, nothing else matters
|
70
|
-
auth.
|
76
|
+
auth.change_pw(password=forcepassword)
|
71
77
|
print("Password has been set.")
|
72
|
-
elif auth.
|
73
|
-
#
|
78
|
+
elif auth.still_has_factory_default_user():
|
79
|
+
# this means we've never set this up properly (eg first start after update)
|
74
80
|
while True:
|
75
81
|
newpw = prompt("Please set a password for web backend access. Leave this empty to generate a random password.",skip=SKIP,secret=True)
|
76
82
|
if newpw is None:
|
@@ -81,7 +87,7 @@ def setup():
|
|
81
87
|
newpw_repeat = prompt("Please type again to confirm.",skip=SKIP,secret=True)
|
82
88
|
if newpw != newpw_repeat: print("Passwords do not match!")
|
83
89
|
else: break
|
84
|
-
auth.
|
90
|
+
auth.change_pw(password=newpw)
|
85
91
|
|
86
92
|
except EOFError:
|
87
93
|
print("No user input possible. If you are running inside a container, set the environment variable",col['yellow']("MALOJA_SKIP_SETUP=yes"))
|
@@ -75,7 +75,7 @@
|
|
75
75
|
<a href="/"><img style="display:block;" src="/favicon.png" /></a>
|
76
76
|
</div>
|
77
77
|
<div id="right-side">
|
78
|
-
<span><input id="searchinput" placeholder="Search for an artist or track..." oninput="search(this)" onblur="clearresults()" /></span>
|
78
|
+
<span><input id="searchinput" placeholder="Search for an album, artist or track..." oninput="search(this)" onblur="clearresults()" /></span>
|
79
79
|
</div>
|
80
80
|
|
81
81
|
|
@@ -67,9 +67,9 @@
|
|
67
67
|
<li>manually scrobble from track pages</li>
|
68
68
|
<li>delete scrobbles</li>
|
69
69
|
<li>reparse scrobbles</li>
|
70
|
-
<li>edit tracks and artists</li>
|
71
|
-
<li>merge tracks and artists</li>
|
72
|
-
<li>upload artist and track art by dropping a file on the existing image on an artist or track page</li>
|
70
|
+
<li>edit tracks, albums and artists</li>
|
71
|
+
<li>merge tracks, albums and artists</li>
|
72
|
+
<li>upload artist, album and track art by dropping a file on the existing image on an artist or track page</li>
|
73
73
|
<li>see more detailed error pages</li>
|
74
74
|
</ul>
|
75
75
|
|
@@ -56,7 +56,7 @@
|
|
56
56
|
|
57
57
|
If you use a Chromium-based browser and listen to music on Plex, Spotify, Soundcloud, Bandcamp or YouTube Music, download the extension and simply enter the server URL as well as your API key in the relevant fields. They will turn green if the server is accessible.
|
58
58
|
<br/><br/>
|
59
|
-
You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter <span class="stats"><span name="serverurl">yourserver.tld</span>/apis/audioscrobbler</span> as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use <span class="stats"><span name="serverurl">yourserver.tld</span>/apis/listenbrainz</span> as the API URL and your API key as token.
|
59
|
+
You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter <span class="stats"><span name="serverurl">yourserver.tld</span>/apis/audioscrobbler</span> as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use <span class="stats"><span name="serverurl">yourserver.tld</span>/apis/listenbrainz</span> as the API URL (depending on the implementation, you might need to add a <span class="stats">/1</span> at the end) and your API key as token.
|
60
60
|
<br/><br/>
|
61
61
|
If you use another browser or another music player, you could try to code your own extension. The API is super simple! Just send a POST HTTP request to
|
62
62
|
|
@@ -29,7 +29,7 @@
|
|
29
29
|
{% for entry in dbc.get_charts_albums(filterkeys,limitkeys,{'only_own_albums':False}) %}
|
30
30
|
|
31
31
|
|
32
|
-
{% if artist not in (entry.album.artists or []) %}
|
32
|
+
{% if info.artist not in (entry.album.artists or []) %}
|
33
33
|
|
34
34
|
{%- set cert = None -%}
|
35
35
|
{%- if entry.scrobbles >= settings.scrobbles_gold_album -%}{% set cert = 'gold' %}{%- endif -%}
|
@@ -20,11 +20,11 @@
|
|
20
20
|
<td class='searchProvider'>{{ links.link_search(entity) }}</td>
|
21
21
|
{% endif %}
|
22
22
|
<td class='track'>
|
23
|
-
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
23
|
+
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists, restrict_amount=True) }}</span> – {{ links.link(entity) }}
|
24
24
|
</td>
|
25
25
|
{% elif entity is mapping and 'albumtitle' in entity %}
|
26
26
|
<td class='album'>
|
27
|
-
<span class='
|
27
|
+
<span class='artist_in_albumcolumn'>{{ links.links(entity.artists, restrict_amount=True) }}</span> – {{ links.link(entity) }}
|
28
28
|
</td>
|
29
29
|
{% else %}
|
30
30
|
<td class='artist'>{{ links.link(entity) }}
|
@@ -8,9 +8,11 @@
|
|
8
8
|
<a href="{{ url(entity) }}">{{ name | e }}</a>
|
9
9
|
{%- endmacro %}
|
10
10
|
|
11
|
-
{% macro links(entities) -%}
|
11
|
+
{% macro links(entities, restrict_amount=False) -%}
|
12
12
|
{% if entities is none or entities == [] %}
|
13
13
|
{{ settings["DEFAULT_ALBUM_ARTIST"] }}
|
14
|
+
{% elif entities.__len__() > 3 and restrict_amount %}
|
15
|
+
{{ link(entities[0]) }} et al.
|
14
16
|
{% else %}
|
15
17
|
{% for entity in entities -%}
|
16
18
|
{{ link(entity) }}{{ ", " if not loop.last }}
|
maloja/web/static/css/maloja.css
CHANGED
@@ -363,12 +363,14 @@ div#notification_area {
|
|
363
363
|
right:20px;
|
364
364
|
}
|
365
365
|
div#notification_area div.notification {
|
366
|
-
background-color:
|
366
|
+
background-color:black;
|
367
367
|
width:400px;
|
368
368
|
min-height:50px;
|
369
369
|
margin-bottom:7px;
|
370
370
|
padding:9px;
|
371
|
-
opacity:0.
|
371
|
+
opacity:0.5;
|
372
|
+
border-left: 8px solid var(--notification-color);
|
373
|
+
border-radius: 3px;
|
372
374
|
}
|
373
375
|
div#notification_area div.notification:hover {
|
374
376
|
opacity:0.95;
|
@@ -781,6 +783,9 @@ table.list td.artists,td.artist,td.title,td.track {
|
|
781
783
|
table.list td.track span.artist_in_trackcolumn {
|
782
784
|
color: var(--text-color-secondary);
|
783
785
|
}
|
786
|
+
table.list td.album span.artist_in_albumcolumn {
|
787
|
+
color: var(--text-color-secondary);
|
788
|
+
}
|
784
789
|
|
785
790
|
table.list td.searchProvider {
|
786
791
|
width: 20px;
|
@@ -987,6 +992,7 @@ table.misc td {
|
|
987
992
|
|
988
993
|
|
989
994
|
div.tiles {
|
995
|
+
max-height: 600px;
|
990
996
|
display: grid;
|
991
997
|
grid-template-columns: repeat(18, calc(100% / 18));
|
992
998
|
grid-template-rows: repeat(6, calc(100% / 6));
|
@@ -22,8 +22,8 @@ div#startpage {
|
|
22
22
|
|
23
23
|
@media (min-width: 1401px) and (max-width: 2200px) {
|
24
24
|
div#startpage {
|
25
|
-
grid-template-columns:
|
26
|
-
grid-template-rows:
|
25
|
+
grid-template-columns: repeat(2, 45vw);
|
26
|
+
grid-template-rows: repeat(3, 45vh);
|
27
27
|
|
28
28
|
grid-template-areas:
|
29
29
|
"charts_artists lastscrobbles"
|
@@ -126,7 +126,7 @@ function scrobble(artists,title,albumartists,album,timestamp) {
|
|
126
126
|
lastArtists = artists;
|
127
127
|
lastTrack = title;
|
128
128
|
lastAlbum = album;
|
129
|
-
lastAlbumartists = albumartists;
|
129
|
+
lastAlbumartists = albumartists || [];
|
130
130
|
|
131
131
|
var payload = {
|
132
132
|
"artists":artists,
|
@@ -1,12 +1,14 @@
|
|
1
1
|
// JS for feedback to the user whenever any XHTTP action is taken
|
2
2
|
|
3
3
|
const colors = {
|
4
|
-
|
4
|
+
'error': 'red',
|
5
|
+
'warning':'#8ACC26',
|
5
6
|
'info':'green'
|
6
7
|
}
|
7
8
|
|
9
|
+
|
8
10
|
const notification_template = info => `
|
9
|
-
<div class="notification" style="
|
11
|
+
<div class="notification" style="--notification-color: ${colors[info.notification_type]};">
|
10
12
|
<b>${info.title}</b><br/>
|
11
13
|
<span>${info.body}</span>
|
12
14
|
|
@@ -35,18 +37,24 @@ function notify(title,msg,notification_type='info',reload=false) {
|
|
35
37
|
}
|
36
38
|
|
37
39
|
function notifyCallback(request) {
|
38
|
-
var
|
40
|
+
var response = request.response;
|
39
41
|
var status = request.status;
|
40
42
|
|
41
43
|
if (status == 200) {
|
42
|
-
|
44
|
+
if (response.hasOwnProperty('warnings') && response.warnings.length > 0) {
|
45
|
+
var notification_type = 'warning';
|
46
|
+
}
|
47
|
+
else {
|
48
|
+
var notification_type = 'info';
|
49
|
+
}
|
50
|
+
|
43
51
|
var title = "Success!";
|
44
|
-
var msg =
|
52
|
+
var msg = response.desc || response;
|
45
53
|
}
|
46
54
|
else {
|
47
|
-
var notification_type = '
|
48
|
-
var title = "Error: " +
|
49
|
-
var msg =
|
55
|
+
var notification_type = 'error';
|
56
|
+
var title = "Error: " + response.error.type;
|
57
|
+
var msg = response.error.desc || "";
|
50
58
|
}
|
51
59
|
|
52
60
|
|