malojaserver 3.2.2__py3-none-any.whl → 3.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maloja/__main__.py +1 -94
- 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 +8 -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 +68 -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 +30 -28
- maloja/proccontrol/tasks/export.py +2 -1
- maloja/proccontrol/tasks/import_scrobbles.py +110 -47
- maloja/server.py +11 -10
- maloja/setup.py +29 -17
- maloja/web/jinja/abstracts/base.jinja +1 -1
- maloja/web/jinja/admin_albumless.jinja +2 -0
- maloja/web/jinja/admin_issues.jinja +2 -2
- maloja/web/jinja/admin_overview.jinja +3 -3
- maloja/web/jinja/admin_setup.jinja +3 -3
- 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 +2 -2
- maloja/web/static/js/notifications.js +16 -8
- maloja/web/static/js/search.js +18 -12
- maloja/web/static/js/upload.js +1 -1
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.4.dist-info}/METADATA +24 -72
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.4.dist-info}/RECORD +42 -42
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.4.dist-info}/WHEEL +1 -1
- /maloja/data_files/state/{scrobbles → import}/dummy +0 -0
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.4.dist-info}/LICENSE +0 -0
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.4.dist-info}/entry_points.txt +0 -0
maloja/database/__init__.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# server
|
2
2
|
from bottle import request, response, FormsDict
|
3
3
|
|
4
|
+
from ..pkg_global import conf
|
5
|
+
|
4
6
|
|
5
7
|
# decorator that makes sure this function is only run in normal operation,
|
6
8
|
# not when we run a task that needs to access the database
|
@@ -27,7 +29,6 @@ from . import exceptions
|
|
27
29
|
|
28
30
|
# doreah toolkit
|
29
31
|
from doreah.logging import log
|
30
|
-
from doreah.auth import authenticated_api, authenticated_api_with_alternate
|
31
32
|
import doreah
|
32
33
|
|
33
34
|
|
@@ -42,6 +43,7 @@ from collections import namedtuple
|
|
42
43
|
from threading import Lock
|
43
44
|
import yaml, json
|
44
45
|
import math
|
46
|
+
from itertools import takewhile
|
45
47
|
|
46
48
|
# url handling
|
47
49
|
import urllib
|
@@ -318,7 +320,7 @@ def associate_tracks_to_album(target_id,source_ids):
|
|
318
320
|
if target_id:
|
319
321
|
target = sqldb.get_album(target_id)
|
320
322
|
log(f"Adding {sources} into {target}")
|
321
|
-
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
|
323
|
+
sqldb.add_tracks_to_albums({src:target_id for src in source_ids},replace=True)
|
322
324
|
else:
|
323
325
|
sqldb.remove_album(source_ids)
|
324
326
|
result = {'sources':sources,'target':target}
|
@@ -444,10 +446,11 @@ def get_charts_albums(dbconn=None,resolve_ids=True,only_own_albums=False,**keys)
|
|
444
446
|
(since,to) = keys.get('timerange').timestamps()
|
445
447
|
|
446
448
|
if 'artist' in keys:
|
447
|
-
|
449
|
+
artist = sqldb.get_artist(sqldb.get_artist_id(keys['artist']))
|
450
|
+
result = sqldb.count_scrobbles_by_album_combined(since=since,to=to,artist=artist,associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
|
448
451
|
if only_own_albums:
|
449
452
|
# TODO: this doesnt take associated into account and doesnt change ranks
|
450
|
-
result = [e for e in result if
|
453
|
+
result = [e for e in result if artist in (e['album']['artists'] or [])]
|
451
454
|
else:
|
452
455
|
result = sqldb.count_scrobbles_by_album(since=since,to=to,resolve_ids=resolve_ids,dbconn=dbconn)
|
453
456
|
return result
|
@@ -570,7 +573,7 @@ def get_performance(dbconn=None,**keys):
|
|
570
573
|
return results
|
571
574
|
|
572
575
|
@waitfordb
|
573
|
-
def get_top_artists(dbconn=None,**keys):
|
576
|
+
def get_top_artists(dbconn=None,compatibility=True,**keys):
|
574
577
|
|
575
578
|
separate = keys.get('separate')
|
576
579
|
|
@@ -578,42 +581,73 @@ def get_top_artists(dbconn=None,**keys):
|
|
578
581
|
results = []
|
579
582
|
|
580
583
|
for rng in rngs:
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
584
|
+
result = {'range':rng}
|
585
|
+
res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)
|
586
|
+
|
587
|
+
result['top'] = [
|
588
|
+
{'artist': r['artist'], 'scrobbles': r['scrobbles'], 'real_scrobbles':r['real_scrobbles'], 'associated_artists': sqldb.get_associated_artists(r['artist'])}
|
589
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
590
|
+
]
|
591
|
+
# for third party applications
|
592
|
+
if compatibility:
|
593
|
+
if result['top']:
|
594
|
+
result.update(result['top'][0])
|
595
|
+
else:
|
596
|
+
result.update({'artist':None,'scrobbles':0,'real_scrobbles':0})
|
597
|
+
|
598
|
+
results.append(result)
|
586
599
|
|
587
600
|
return results
|
588
601
|
|
589
602
|
|
590
603
|
@waitfordb
|
591
|
-
def get_top_tracks(dbconn=None,**keys):
|
604
|
+
def get_top_tracks(dbconn=None,compatibility=True,**keys):
|
592
605
|
|
593
606
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
594
607
|
results = []
|
595
608
|
|
596
609
|
for rng in rngs:
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
610
|
+
result = {'range':rng}
|
611
|
+
res = get_charts_tracks(timerange=rng,dbconn=dbconn)
|
612
|
+
|
613
|
+
result['top'] = [
|
614
|
+
{'track': r['track'], 'scrobbles': r['scrobbles']}
|
615
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
616
|
+
]
|
617
|
+
# for third party applications
|
618
|
+
if compatibility:
|
619
|
+
if result['top']:
|
620
|
+
result.update(result['top'][0])
|
621
|
+
else:
|
622
|
+
result.update({'track':None,'scrobbles':0})
|
623
|
+
|
624
|
+
results.append(result)
|
602
625
|
|
603
626
|
return results
|
604
627
|
|
605
628
|
@waitfordb
|
606
|
-
def get_top_albums(dbconn=None,**keys):
|
629
|
+
def get_top_albums(dbconn=None,compatibility=True,**keys):
|
607
630
|
|
608
631
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
609
632
|
results = []
|
610
633
|
|
611
634
|
for rng in rngs:
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
635
|
+
|
636
|
+
result = {'range':rng}
|
637
|
+
res = get_charts_albums(timerange=rng,dbconn=dbconn)
|
638
|
+
|
639
|
+
result['top'] = [
|
640
|
+
{'album': r['album'], 'scrobbles': r['scrobbles']}
|
641
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
642
|
+
]
|
643
|
+
# for third party applications
|
644
|
+
if compatibility:
|
645
|
+
if result['top']:
|
646
|
+
result.update(result['top'][0])
|
647
|
+
else:
|
648
|
+
result.update({'album':None,'scrobbles':0})
|
649
|
+
|
650
|
+
results.append(result)
|
617
651
|
|
618
652
|
return results
|
619
653
|
|
@@ -900,6 +934,9 @@ def get_predefined_rulesets(dbconn=None):
|
|
900
934
|
|
901
935
|
|
902
936
|
def start_db():
|
937
|
+
|
938
|
+
conf.AUX_MODE = True # that is, without a doubt, the worst python code you have ever seen
|
939
|
+
|
903
940
|
# Upgrade database
|
904
941
|
from .. import upgrade
|
905
942
|
upgrade.upgrade_db(sqldb.add_scrobbles)
|
@@ -909,11 +946,19 @@ def start_db():
|
|
909
946
|
from . import associated
|
910
947
|
associated.load_associated_rules()
|
911
948
|
|
949
|
+
# import scrobbles
|
950
|
+
from ..proccontrol.tasks.import_scrobbles import import_scrobbles #lmao this codebase is so fucked
|
951
|
+
for f in os.listdir(data_dir['import']()):
|
952
|
+
if f != 'dummy':
|
953
|
+
import_scrobbles(data_dir['import'](f))
|
954
|
+
|
912
955
|
dbstatus['healthy'] = True
|
913
956
|
|
957
|
+
conf.AUX_MODE = False # but you have seen it
|
958
|
+
|
914
959
|
# inform time module about begin of scrobbling
|
915
960
|
try:
|
916
|
-
firstscrobble = sqldb.get_scrobbles()[0]
|
961
|
+
firstscrobble = sqldb.get_scrobbles(limit=1)[0]
|
917
962
|
register_scrobbletime(firstscrobble['time'])
|
918
963
|
except IndexError:
|
919
964
|
register_scrobbletime(int(datetime.datetime.now().timestamp()))
|
maloja/database/associated.py
CHANGED
@@ -19,12 +19,16 @@ def load_associated_rules():
|
|
19
19
|
|
20
20
|
# load from file
|
21
21
|
rawrules = []
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
try:
|
23
|
+
for f in os.listdir(data_dir["rules"]()):
|
24
|
+
if f.split('.')[-1].lower() != 'tsv': continue
|
25
|
+
filepath = data_dir["rules"](f)
|
26
|
+
with open(filepath,'r') as filed:
|
27
|
+
reader = csv.reader(filed,delimiter="\t")
|
28
|
+
rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
|
29
|
+
except FileNotFoundError:
|
30
|
+
return
|
31
|
+
|
28
32
|
rules = [{'source_artist':r[1],'target_artist':r[2]} for r in rawrules if r[0]=="countas"]
|
29
33
|
|
30
34
|
#for rule in rules:
|
maloja/database/exceptions.py
CHANGED
@@ -1,37 +1,57 @@
|
|
1
1
|
from bottle import HTTPError
|
2
2
|
|
3
|
+
|
3
4
|
class EntityExists(Exception):
|
4
|
-
def __init__(self,entitydict):
|
5
|
+
def __init__(self, entitydict):
|
5
6
|
self.entitydict = entitydict
|
6
7
|
|
7
8
|
|
8
9
|
class TrackExists(EntityExists):
|
9
10
|
pass
|
10
11
|
|
12
|
+
|
11
13
|
class ArtistExists(EntityExists):
|
12
14
|
pass
|
13
15
|
|
16
|
+
|
14
17
|
class AlbumExists(EntityExists):
|
15
18
|
pass
|
16
19
|
|
20
|
+
|
21
|
+
# if the scrobbles dont match
|
22
|
+
class DuplicateTimestamp(Exception):
|
23
|
+
def __init__(self, existing_scrobble, rejected_scrobble):
|
24
|
+
self.existing_scrobble = existing_scrobble
|
25
|
+
self.rejected_scrobble = rejected_scrobble
|
26
|
+
|
27
|
+
|
28
|
+
# if it's the same scrobble
|
29
|
+
class DuplicateScrobble(Exception):
|
30
|
+
def __init__(self, scrobble):
|
31
|
+
self.scrobble = scrobble
|
32
|
+
|
33
|
+
|
17
34
|
class DatabaseNotBuilt(HTTPError):
|
18
35
|
def __init__(self):
|
19
36
|
super().__init__(
|
20
37
|
status=503,
|
21
38
|
body="The Maloja Database is being upgraded to support new Maloja features. This could take a while.",
|
22
|
-
headers={"Retry-After":120}
|
39
|
+
headers={"Retry-After": 120}
|
23
40
|
)
|
24
41
|
|
25
42
|
|
26
43
|
class MissingScrobbleParameters(Exception):
|
27
|
-
def __init__(self,params=[]):
|
44
|
+
def __init__(self, params=[]):
|
28
45
|
self.params = params
|
29
46
|
|
47
|
+
|
30
48
|
class MissingEntityParameter(Exception):
|
31
49
|
pass
|
32
50
|
|
51
|
+
|
33
52
|
class EntityDoesNotExist(HTTPError):
|
34
53
|
entitytype = 'Entity'
|
54
|
+
|
35
55
|
def __init__(self,entitydict):
|
36
56
|
self.entitydict = entitydict
|
37
57
|
super().__init__(
|
@@ -39,9 +59,14 @@ class EntityDoesNotExist(HTTPError):
|
|
39
59
|
body=f"The {self.entitytype} '{self.entitydict}' does not exist in the database."
|
40
60
|
)
|
41
61
|
|
62
|
+
|
42
63
|
class ArtistDoesNotExist(EntityDoesNotExist):
|
43
64
|
entitytype = 'Artist'
|
65
|
+
|
66
|
+
|
44
67
|
class AlbumDoesNotExist(EntityDoesNotExist):
|
45
68
|
entitytype = 'Album'
|
69
|
+
|
70
|
+
|
46
71
|
class TrackDoesNotExist(EntityDoesNotExist):
|
47
72
|
entitytype = 'Track'
|