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.
Files changed (38) hide show
  1. maloja/__main__.py +1 -1
  2. maloja/__pkginfo__.py +1 -1
  3. maloja/apis/_base.py +26 -19
  4. maloja/apis/_exceptions.py +1 -1
  5. maloja/apis/audioscrobbler.py +35 -7
  6. maloja/apis/audioscrobbler_legacy.py +5 -5
  7. maloja/apis/listenbrainz.py +7 -5
  8. maloja/apis/native_v1.py +43 -26
  9. maloja/cleanup.py +9 -7
  10. maloja/data_files/config/rules/predefined/krateng_kpopgirlgroups.tsv +2 -2
  11. maloja/database/__init__.py +55 -23
  12. maloja/database/associated.py +10 -6
  13. maloja/database/exceptions.py +28 -3
  14. maloja/database/sqldb.py +216 -168
  15. maloja/dev/profiler.py +3 -4
  16. maloja/images.py +6 -0
  17. maloja/malojauri.py +2 -0
  18. maloja/pkg_global/conf.py +29 -28
  19. maloja/proccontrol/tasks/export.py +2 -1
  20. maloja/proccontrol/tasks/import_scrobbles.py +57 -15
  21. maloja/server.py +4 -5
  22. maloja/setup.py +13 -7
  23. maloja/web/jinja/abstracts/base.jinja +1 -1
  24. maloja/web/jinja/admin_albumless.jinja +2 -0
  25. maloja/web/jinja/admin_overview.jinja +3 -3
  26. maloja/web/jinja/admin_setup.jinja +1 -1
  27. maloja/web/jinja/partials/album_showcase.jinja +1 -1
  28. maloja/web/jinja/snippets/entityrow.jinja +2 -2
  29. maloja/web/jinja/snippets/links.jinja +3 -1
  30. maloja/web/static/css/maloja.css +8 -2
  31. maloja/web/static/css/startpage.css +2 -2
  32. maloja/web/static/js/manualscrobble.js +1 -1
  33. maloja/web/static/js/notifications.js +16 -8
  34. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
  35. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/RECORD +38 -38
  36. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
  37. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
  38. {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
- clock = Clock()
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 = clock.stop()
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.mknod(pthj(pth,".test"))
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'], "Which metadata providers should be used in what order. Musicbrainz is rate-limited and should not be used first."),
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",path,"is not usable.")
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
- data_dir = {
329
- k:lambda *x,k=k: pthj(data_directories[k],*x) for k in data_directories
330
- }
331
-
332
-
339
+ def __getitem__(self, key):
340
+ return lambda *x, k=key: pthj(self.dirs[k], *x)
333
341
 
334
- ### DOREAH CONFIGURATION
342
+ data_dir = DataDirs(data_directories)
335
343
 
336
- from doreah import config
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
- custom_css_files = [f for f in os.listdir(data_dir['css']()) if f.lower().endswith('.css')]
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
- if re.match(r".*\.csv",filename):
37
- typeid,typedesc = "lastfm","Last.fm"
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
- else:
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.check(request)
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.authenticated
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.defaultuser.setpw(forcepassword)
76
+ auth.change_pw(password=forcepassword)
71
77
  print("Password has been set.")
72
- elif auth.defaultuser.checkpw("admin"):
73
- # if the actual pw is admin, it means we've never set this up properly (eg first start after update)
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.defaultuser.setpw(newpw)
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
 
@@ -6,6 +6,8 @@
6
6
  Here you can find tracks that currently have no album.<br/><br/>
7
7
 
8
8
  {% with list = dbc.get_tracks_without_album() %}
9
+ You have {{list|length}} tracks with no album.<br/><br/>
10
+
9
11
  {% include 'partials/list_tracks.jinja' %}
10
12
  {% endwith %}
11
13
 
@@ -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='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
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 }}
@@ -363,12 +363,14 @@ div#notification_area {
363
363
  right:20px;
364
364
  }
365
365
  div#notification_area div.notification {
366
- background-color:white;
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.4;
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: 45vw 45vw;
26
- grid-template-rows: 45vh 45vh 45vh;
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
- 'warning':'red',
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="background-color:${colors[info.notification_type]};">
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 body = request.response;
40
+ var response = request.response;
39
41
  var status = request.status;
40
42
 
41
43
  if (status == 200) {
42
- var notification_type = 'info';
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 = body.desc || body;
52
+ var msg = response.desc || response;
45
53
  }
46
54
  else {
47
- var notification_type = 'warning';
48
- var title = "Error: " + body.error.type;
49
- var msg = body.error.desc || "";
55
+ var notification_type = 'error';
56
+ var title = "Error: " + response.error.type;
57
+ var msg = response.error.desc || "";
50
58
  }
51
59
 
52
60