malojaserver 3.2.2__py3-none-any.whl → 3.2.3__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 -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/__main__.py
CHANGED
@@ -135,7 +135,7 @@ def debug():
|
|
135
135
|
def print_info():
|
136
136
|
print_header_info()
|
137
137
|
print(col['lightblue']("Configuration Directory:"),conf.dir_settings['config'])
|
138
|
-
print(col['lightblue']("
|
138
|
+
print(col['lightblue']("State Directory: "),conf.dir_settings['state'])
|
139
139
|
print(col['lightblue']("Log Directory: "),conf.dir_settings['logs'])
|
140
140
|
print(col['lightblue']("Network: "),f"Dual Stack, Port {conf.malojaconfig['port']}" if conf.malojaconfig['host'] == "*" else f"IPv{ip_address(conf.malojaconfig['host']).version}, Port {conf.malojaconfig['port']}")
|
141
141
|
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
maloja/__pkginfo__.py
CHANGED
maloja/apis/_base.py
CHANGED
@@ -25,9 +25,20 @@ __logmodulename__ = "apis"
|
|
25
25
|
|
26
26
|
cla = CleanerAgent()
|
27
27
|
|
28
|
+
|
29
|
+
|
30
|
+
# wrapper method: calls handle. final net to catch exceptions and map them to the handlers proper json / xml response
|
31
|
+
# handle method: finds the method for this path / query. can only raise InvalidMethodException
|
32
|
+
# scrobble: NOT the exposed scrobble method - helper for all APIs to scrobble their results with self-identification
|
33
|
+
|
34
|
+
|
28
35
|
class APIHandler:
|
36
|
+
|
37
|
+
__apiname__: str
|
38
|
+
errors: dict
|
29
39
|
# make these classes singletons
|
30
40
|
_instance = None
|
41
|
+
|
31
42
|
def __new__(cls, *args, **kwargs):
|
32
43
|
if not isinstance(cls._instance, cls):
|
33
44
|
cls._instance = object.__new__(cls, *args, **kwargs)
|
@@ -62,37 +73,33 @@ class APIHandler:
|
|
62
73
|
|
63
74
|
try:
|
64
75
|
response.status,result = self.handle(path,keys)
|
65
|
-
except Exception:
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
76
|
+
except Exception as e:
|
77
|
+
for exc_type, exc_response in self.errors.items():
|
78
|
+
if isinstance(e, exc_type):
|
79
|
+
response.status, result = exc_response
|
80
|
+
log(f"Error with {self.__apiname__} API: {e} (Request: {path})")
|
81
|
+
break
|
70
82
|
else:
|
71
|
-
|
72
|
-
|
83
|
+
# THIS SHOULD NOT HAPPEN
|
84
|
+
response.status, result = 500, {"status": "Unknown error", "code": 500}
|
85
|
+
log(f"Unhandled Exception with {self.__apiname__} API: {e} (Request: {path})")
|
73
86
|
|
74
87
|
return result
|
75
|
-
#else:
|
76
|
-
# result = {"error":"Invalid scrobble protocol"}
|
77
|
-
# response.status = 500
|
78
88
|
|
79
89
|
|
80
90
|
def handle(self,path,keys):
|
81
91
|
|
82
92
|
try:
|
83
|
-
methodname = self.get_method(path,keys)
|
93
|
+
methodname = self.get_method(path, keys)
|
84
94
|
method = self.methods[methodname]
|
85
|
-
except
|
86
|
-
log("Could not find a handler for method
|
87
|
-
log("Keys: "
|
95
|
+
except KeyError:
|
96
|
+
log(f"Could not find a handler for method {methodname} in API {self.__apiname__}", module="debug")
|
97
|
+
log(f"Keys: {keys}", module="debug")
|
88
98
|
raise InvalidMethodException()
|
89
|
-
return method(path,keys)
|
99
|
+
return method(path, keys)
|
90
100
|
|
91
101
|
|
92
102
|
def scrobble(self,rawscrobble,client=None):
|
93
103
|
|
94
104
|
# fixing etc is handled by the main scrobble function
|
95
|
-
|
96
|
-
return database.incoming_scrobble(rawscrobble,api=self.__apiname__,client=client)
|
97
|
-
except Exception:
|
98
|
-
raise ScrobblingException()
|
105
|
+
return database.incoming_scrobble(rawscrobble,api=self.__apiname__,client=client)
|
maloja/apis/_exceptions.py
CHANGED
maloja/apis/audioscrobbler.py
CHANGED
@@ -21,13 +21,22 @@ class Audioscrobbler(APIHandler):
|
|
21
21
|
"track.scrobble":self.submit_scrobble
|
22
22
|
}
|
23
23
|
self.errors = {
|
24
|
-
BadAuthException:(400,{"error":6,"message":"Requires authentication"}),
|
25
|
-
InvalidAuthException:(401,{"error":4,"message":"Invalid credentials"}),
|
26
|
-
InvalidMethodException:(200,{"error":3,"message":"Invalid method"}),
|
27
|
-
InvalidSessionKey:(403,{"error":9,"message":"Invalid session key"}),
|
28
|
-
|
24
|
+
BadAuthException: (400, {"error": 6, "message": "Requires authentication"}),
|
25
|
+
InvalidAuthException: (401, {"error": 4, "message": "Invalid credentials"}),
|
26
|
+
InvalidMethodException: (200, {"error": 3, "message": "Invalid method"}),
|
27
|
+
InvalidSessionKey: (403, {"error": 9, "message": "Invalid session key"}),
|
28
|
+
Exception: (500, {"error": 8, "message": "Operation failed"})
|
29
29
|
}
|
30
30
|
|
31
|
+
# xml string escaping: https://stackoverflow.com/a/28703510
|
32
|
+
def xml_escape(self, str_xml: str):
|
33
|
+
str_xml = str_xml.replace("&", "&")
|
34
|
+
str_xml = str_xml.replace("<", "<")
|
35
|
+
str_xml = str_xml.replace("<", "<")
|
36
|
+
str_xml = str_xml.replace("\"", """)
|
37
|
+
str_xml = str_xml.replace("'", "'")
|
38
|
+
return str_xml
|
39
|
+
|
31
40
|
def get_method(self,pathnodes,keys):
|
32
41
|
return keys.get("method")
|
33
42
|
|
@@ -45,12 +54,22 @@ class Audioscrobbler(APIHandler):
|
|
45
54
|
token = keys.get("authToken")
|
46
55
|
user = keys.get("username")
|
47
56
|
password = keys.get("password")
|
57
|
+
format = keys.get("format") or "xml" # Audioscrobbler 2.0 uses XML by default
|
48
58
|
# either username and password
|
49
59
|
if user is not None and password is not None:
|
50
60
|
client = apikeystore.check_and_identify_key(password)
|
51
61
|
if client:
|
52
62
|
sessionkey = self.generate_key(client)
|
53
|
-
|
63
|
+
if format == "json":
|
64
|
+
return 200,{"session":{"key":sessionkey}}
|
65
|
+
else:
|
66
|
+
return 200,"""<lfm status="ok">
|
67
|
+
<session>
|
68
|
+
<name>%s</name>
|
69
|
+
<key>%s</key>
|
70
|
+
<subscriber>0</subscriber>
|
71
|
+
</session>
|
72
|
+
</lfm>""" % (self.xml_escape(user), self.xml_escape(sessionkey))
|
54
73
|
else:
|
55
74
|
raise InvalidAuthException()
|
56
75
|
# or username and token (deprecated by lastfm)
|
@@ -59,7 +78,16 @@ class Audioscrobbler(APIHandler):
|
|
59
78
|
key = apikeystore[client]
|
60
79
|
if md5(user + md5(key)) == token:
|
61
80
|
sessionkey = self.generate_key(client)
|
62
|
-
|
81
|
+
if format == "json":
|
82
|
+
return 200,{"session":{"key":sessionkey}}
|
83
|
+
else:
|
84
|
+
return 200,"""<lfm status="ok">
|
85
|
+
<session>
|
86
|
+
<name>%s</name>
|
87
|
+
<key>%s</key>
|
88
|
+
<subscriber>0</subscriber>
|
89
|
+
</session>
|
90
|
+
</lfm>""" % (self.xml_escape(user), self.xml_escape(sessionkey))
|
63
91
|
raise InvalidAuthException()
|
64
92
|
else:
|
65
93
|
raise BadAuthException()
|
@@ -23,11 +23,11 @@ class AudioscrobblerLegacy(APIHandler):
|
|
23
23
|
"scrobble":self.submit_scrobble
|
24
24
|
}
|
25
25
|
self.errors = {
|
26
|
-
BadAuthException:(403,"BADAUTH\n"),
|
27
|
-
InvalidAuthException:(403,"BADAUTH\n"),
|
28
|
-
InvalidMethodException:(400,"FAILED\n"),
|
29
|
-
InvalidSessionKey:(403,"BADSESSION\n"),
|
30
|
-
|
26
|
+
BadAuthException: (403, "BADAUTH\n"),
|
27
|
+
InvalidAuthException: (403, "BADAUTH\n"),
|
28
|
+
InvalidMethodException: (400, "FAILED\n"),
|
29
|
+
InvalidSessionKey: (403, "BADSESSION\n"),
|
30
|
+
Exception: (500, "FAILED\n")
|
31
31
|
}
|
32
32
|
|
33
33
|
def get_method(self,pathnodes,keys):
|
maloja/apis/listenbrainz.py
CHANGED
@@ -3,6 +3,7 @@ from ._exceptions import *
|
|
3
3
|
from .. import database
|
4
4
|
import datetime
|
5
5
|
from ._apikeys import apikeystore
|
6
|
+
from ..database.exceptions import DuplicateScrobble
|
6
7
|
|
7
8
|
from ..pkg_global.conf import malojaconfig
|
8
9
|
|
@@ -21,11 +22,12 @@ class Listenbrainz(APIHandler):
|
|
21
22
|
"validate-token":self.validate_token
|
22
23
|
}
|
23
24
|
self.errors = {
|
24
|
-
BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}),
|
25
|
-
InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}),
|
26
|
-
InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}),
|
27
|
-
MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}),
|
28
|
-
|
25
|
+
BadAuthException: (401, {"code": 401, "error": "You need to provide an Authorization header."}),
|
26
|
+
InvalidAuthException: (401, {"code": 401, "error": "Incorrect Authorization"}),
|
27
|
+
InvalidMethodException: (200, {"code": 200, "error": "Invalid Method"}),
|
28
|
+
MalformedJSONException: (400, {"code": 400, "error": "Invalid JSON document submitted."}),
|
29
|
+
DuplicateScrobble: (200, {"status": "ok"}),
|
30
|
+
Exception: (500, {"code": 500, "error": "Unspecified server error."})
|
29
31
|
}
|
30
32
|
|
31
33
|
def get_method(self,pathnodes,keys):
|
maloja/apis/native_v1.py
CHANGED
@@ -7,7 +7,6 @@ from bottle import response, static_file, FormsDict
|
|
7
7
|
from inspect import signature
|
8
8
|
|
9
9
|
from doreah.logging import log
|
10
|
-
from doreah.auth import authenticated_function
|
11
10
|
|
12
11
|
# nimrodel API
|
13
12
|
from nimrodel import EAPI as API
|
@@ -15,7 +14,7 @@ from nimrodel import Multi
|
|
15
14
|
|
16
15
|
|
17
16
|
from .. import database
|
18
|
-
from ..pkg_global.conf import malojaconfig, data_dir
|
17
|
+
from ..pkg_global.conf import malojaconfig, data_dir, auth
|
19
18
|
|
20
19
|
|
21
20
|
|
@@ -82,6 +81,24 @@ errors = {
|
|
82
81
|
'desc':"This entity does not exist in the database."
|
83
82
|
}
|
84
83
|
}),
|
84
|
+
database.exceptions.DuplicateTimestamp: lambda e: (409,{
|
85
|
+
"status":"error",
|
86
|
+
"error":{
|
87
|
+
'type':'duplicate_timestamp',
|
88
|
+
'value':e.rejected_scrobble,
|
89
|
+
'desc':"A scrobble is already registered with this timestamp."
|
90
|
+
}
|
91
|
+
}),
|
92
|
+
database.exceptions.DuplicateScrobble: lambda e: (200,{
|
93
|
+
"status": "success",
|
94
|
+
"desc": "The scrobble is present in the database.",
|
95
|
+
"track": {},
|
96
|
+
"warnings": [{
|
97
|
+
'type': 'scrobble_exists',
|
98
|
+
'value': None,
|
99
|
+
'desc': 'This scrobble exists in the database (same timestamp and track). The submitted scrobble was not added.'
|
100
|
+
}]
|
101
|
+
}),
|
85
102
|
images.MalformedB64: lambda e: (400,{
|
86
103
|
"status":"failure",
|
87
104
|
"error":{
|
@@ -474,7 +491,7 @@ def get_top_artists_external(k_filter, k_limit, k_delimit, k_amount):
|
|
474
491
|
:rtype: Dictionary"""
|
475
492
|
|
476
493
|
ckeys = {**k_limit, **k_delimit}
|
477
|
-
results = database.get_top_artists(**ckeys)
|
494
|
+
results = database.get_top_artists(**ckeys,compatibility=True)
|
478
495
|
|
479
496
|
return {
|
480
497
|
"status":"ok",
|
@@ -493,7 +510,7 @@ def get_top_tracks_external(k_filter, k_limit, k_delimit, k_amount):
|
|
493
510
|
:rtype: Dictionary"""
|
494
511
|
|
495
512
|
ckeys = {**k_limit, **k_delimit}
|
496
|
-
results = database.get_top_tracks(**ckeys)
|
513
|
+
results = database.get_top_tracks(**ckeys,compatibility=True)
|
497
514
|
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST/ALBUM AS WELL?
|
498
515
|
|
499
516
|
return {
|
@@ -513,7 +530,7 @@ def get_top_albums_external(k_filter, k_limit, k_delimit, k_amount):
|
|
513
530
|
:rtype: Dictionary"""
|
514
531
|
|
515
532
|
ckeys = {**k_limit, **k_delimit}
|
516
|
-
results = database.get_top_albums(**ckeys)
|
533
|
+
results = database.get_top_albums(**ckeys,compatibility=True)
|
517
534
|
# IMPLEMENT THIS FOR TOP ALBUMS OF ARTIST AS WELL?
|
518
535
|
|
519
536
|
return {
|
@@ -567,7 +584,7 @@ def album_info_external(k_filter, k_limit, k_delimit, k_amount):
|
|
567
584
|
|
568
585
|
|
569
586
|
@api.post("newscrobble")
|
570
|
-
@authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
587
|
+
@auth.authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
571
588
|
@catch_exceptions
|
572
589
|
def post_scrobble(
|
573
590
|
artist:Multi=None,
|
@@ -647,7 +664,7 @@ def post_scrobble(
|
|
647
664
|
|
648
665
|
|
649
666
|
@api.post("addpicture")
|
650
|
-
@authenticated_function(alternate=api_key_correct,api=True)
|
667
|
+
@auth.authenticated_function(alternate=api_key_correct,api=True)
|
651
668
|
@catch_exceptions
|
652
669
|
@convert_kwargs
|
653
670
|
def add_picture(k_filter, k_limit, k_delimit, k_amount, k_special):
|
@@ -670,7 +687,7 @@ def add_picture(k_filter, k_limit, k_delimit, k_amount, k_special):
|
|
670
687
|
|
671
688
|
|
672
689
|
@api.post("importrules")
|
673
|
-
@authenticated_function(api=True)
|
690
|
+
@auth.authenticated_function(api=True)
|
674
691
|
@catch_exceptions
|
675
692
|
def import_rulemodule(**keys):
|
676
693
|
"""Internal Use Only"""
|
@@ -689,7 +706,7 @@ def import_rulemodule(**keys):
|
|
689
706
|
|
690
707
|
|
691
708
|
@api.post("rebuild")
|
692
|
-
@authenticated_function(api=True)
|
709
|
+
@auth.authenticated_function(api=True)
|
693
710
|
@catch_exceptions
|
694
711
|
def rebuild(**keys):
|
695
712
|
"""Internal Use Only"""
|
@@ -765,7 +782,7 @@ def search(**keys):
|
|
765
782
|
|
766
783
|
|
767
784
|
@api.post("newrule")
|
768
|
-
@authenticated_function(api=True)
|
785
|
+
@auth.authenticated_function(api=True)
|
769
786
|
@catch_exceptions
|
770
787
|
def newrule(**keys):
|
771
788
|
"""Internal Use Only"""
|
@@ -776,21 +793,21 @@ def newrule(**keys):
|
|
776
793
|
|
777
794
|
|
778
795
|
@api.post("settings")
|
779
|
-
@authenticated_function(api=True)
|
796
|
+
@auth.authenticated_function(api=True)
|
780
797
|
@catch_exceptions
|
781
798
|
def set_settings(**keys):
|
782
799
|
"""Internal Use Only"""
|
783
800
|
malojaconfig.update(keys)
|
784
801
|
|
785
802
|
@api.post("apikeys")
|
786
|
-
@authenticated_function(api=True)
|
803
|
+
@auth.authenticated_function(api=True)
|
787
804
|
@catch_exceptions
|
788
805
|
def set_apikeys(**keys):
|
789
806
|
"""Internal Use Only"""
|
790
807
|
apikeystore.update(keys)
|
791
808
|
|
792
809
|
@api.post("import")
|
793
|
-
@authenticated_function(api=True)
|
810
|
+
@auth.authenticated_function(api=True)
|
794
811
|
@catch_exceptions
|
795
812
|
def import_scrobbles(identifier):
|
796
813
|
"""Internal Use Only"""
|
@@ -798,7 +815,7 @@ def import_scrobbles(identifier):
|
|
798
815
|
import_scrobbles(identifier)
|
799
816
|
|
800
817
|
@api.get("backup")
|
801
|
-
@authenticated_function(api=True)
|
818
|
+
@auth.authenticated_function(api=True)
|
802
819
|
@catch_exceptions
|
803
820
|
def get_backup(**keys):
|
804
821
|
"""Internal Use Only"""
|
@@ -811,7 +828,7 @@ def get_backup(**keys):
|
|
811
828
|
return static_file(os.path.basename(archivefile),root=tmpfolder)
|
812
829
|
|
813
830
|
@api.get("export")
|
814
|
-
@authenticated_function(api=True)
|
831
|
+
@auth.authenticated_function(api=True)
|
815
832
|
@catch_exceptions
|
816
833
|
def get_export(**keys):
|
817
834
|
"""Internal Use Only"""
|
@@ -825,7 +842,7 @@ def get_export(**keys):
|
|
825
842
|
|
826
843
|
|
827
844
|
@api.post("delete_scrobble")
|
828
|
-
@authenticated_function(api=True)
|
845
|
+
@auth.authenticated_function(api=True)
|
829
846
|
@catch_exceptions
|
830
847
|
def delete_scrobble(timestamp):
|
831
848
|
"""Internal Use Only"""
|
@@ -837,7 +854,7 @@ def delete_scrobble(timestamp):
|
|
837
854
|
|
838
855
|
|
839
856
|
@api.post("edit_artist")
|
840
|
-
@authenticated_function(api=True)
|
857
|
+
@auth.authenticated_function(api=True)
|
841
858
|
@catch_exceptions
|
842
859
|
def edit_artist(id,name):
|
843
860
|
"""Internal Use Only"""
|
@@ -847,7 +864,7 @@ def edit_artist(id,name):
|
|
847
864
|
}
|
848
865
|
|
849
866
|
@api.post("edit_track")
|
850
|
-
@authenticated_function(api=True)
|
867
|
+
@auth.authenticated_function(api=True)
|
851
868
|
@catch_exceptions
|
852
869
|
def edit_track(id,title):
|
853
870
|
"""Internal Use Only"""
|
@@ -857,7 +874,7 @@ def edit_track(id,title):
|
|
857
874
|
}
|
858
875
|
|
859
876
|
@api.post("edit_album")
|
860
|
-
@authenticated_function(api=True)
|
877
|
+
@auth.authenticated_function(api=True)
|
861
878
|
@catch_exceptions
|
862
879
|
def edit_album(id,albumtitle):
|
863
880
|
"""Internal Use Only"""
|
@@ -868,7 +885,7 @@ def edit_album(id,albumtitle):
|
|
868
885
|
|
869
886
|
|
870
887
|
@api.post("merge_tracks")
|
871
|
-
@authenticated_function(api=True)
|
888
|
+
@auth.authenticated_function(api=True)
|
872
889
|
@catch_exceptions
|
873
890
|
def merge_tracks(target_id,source_ids):
|
874
891
|
"""Internal Use Only"""
|
@@ -879,7 +896,7 @@ def merge_tracks(target_id,source_ids):
|
|
879
896
|
}
|
880
897
|
|
881
898
|
@api.post("merge_artists")
|
882
|
-
@authenticated_function(api=True)
|
899
|
+
@auth.authenticated_function(api=True)
|
883
900
|
@catch_exceptions
|
884
901
|
def merge_artists(target_id,source_ids):
|
885
902
|
"""Internal Use Only"""
|
@@ -890,7 +907,7 @@ def merge_artists(target_id,source_ids):
|
|
890
907
|
}
|
891
908
|
|
892
909
|
@api.post("merge_albums")
|
893
|
-
@authenticated_function(api=True)
|
910
|
+
@auth.authenticated_function(api=True)
|
894
911
|
@catch_exceptions
|
895
912
|
def merge_artists(target_id,source_ids):
|
896
913
|
"""Internal Use Only"""
|
@@ -901,7 +918,7 @@ def merge_artists(target_id,source_ids):
|
|
901
918
|
}
|
902
919
|
|
903
920
|
@api.post("associate_albums_to_artist")
|
904
|
-
@authenticated_function(api=True)
|
921
|
+
@auth.authenticated_function(api=True)
|
905
922
|
@catch_exceptions
|
906
923
|
def associate_albums_to_artist(target_id,source_ids,remove=False):
|
907
924
|
result = database.associate_albums_to_artist(target_id,source_ids,remove=remove)
|
@@ -913,7 +930,7 @@ def associate_albums_to_artist(target_id,source_ids,remove=False):
|
|
913
930
|
}
|
914
931
|
|
915
932
|
@api.post("associate_tracks_to_artist")
|
916
|
-
@authenticated_function(api=True)
|
933
|
+
@auth.authenticated_function(api=True)
|
917
934
|
@catch_exceptions
|
918
935
|
def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
919
936
|
result = database.associate_tracks_to_artist(target_id,source_ids,remove=remove)
|
@@ -925,7 +942,7 @@ def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
|
925
942
|
}
|
926
943
|
|
927
944
|
@api.post("associate_tracks_to_album")
|
928
|
-
@authenticated_function(api=True)
|
945
|
+
@auth.authenticated_function(api=True)
|
929
946
|
@catch_exceptions
|
930
947
|
def associate_tracks_to_album(target_id,source_ids):
|
931
948
|
result = database.associate_tracks_to_album(target_id,source_ids)
|
@@ -937,7 +954,7 @@ def associate_tracks_to_album(target_id,source_ids):
|
|
937
954
|
|
938
955
|
|
939
956
|
@api.post("reparse_scrobble")
|
940
|
-
@authenticated_function(api=True)
|
957
|
+
@auth.authenticated_function(api=True)
|
941
958
|
@catch_exceptions
|
942
959
|
def reparse_scrobble(timestamp):
|
943
960
|
"""Internal Use Only"""
|
maloja/cleanup.py
CHANGED
@@ -15,13 +15,15 @@ class CleanerAgent:
|
|
15
15
|
def updateRules(self):
|
16
16
|
|
17
17
|
rawrules = []
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
try:
|
19
|
+
for f in os.listdir(data_dir["rules"]()):
|
20
|
+
if f.split('.')[-1].lower() != 'tsv': continue
|
21
|
+
filepath = data_dir["rules"](f)
|
22
|
+
with open(filepath,'r') as filed:
|
23
|
+
reader = csv.reader(filed,delimiter="\t")
|
24
|
+
rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
|
25
|
+
except FileNotFoundError:
|
26
|
+
pass
|
25
27
|
|
26
28
|
self.rules_belongtogether = [r[1] for r in rawrules if r[0]=="belongtogether"]
|
27
29
|
self.rules_notanartist = [r[1] for r in rawrules if r[0]=="notanartist"]
|
@@ -160,8 +160,8 @@ replaceartist 여자친구 GFriend GFriend
|
|
160
160
|
# Girl's Generation
|
161
161
|
replaceartist 소녀시대 Girls' Generation
|
162
162
|
replaceartist SNSD Girls' Generation
|
163
|
-
replaceartist
|
164
|
-
countas
|
163
|
+
replaceartist Girls' Generation-TTS TaeTiSeo
|
164
|
+
countas TaeTiSeo Girls' Generation
|
165
165
|
|
166
166
|
# Apink
|
167
167
|
replaceartist A Pink Apink
|
maloja/database/__init__.py
CHANGED
@@ -27,7 +27,6 @@ from . import exceptions
|
|
27
27
|
|
28
28
|
# doreah toolkit
|
29
29
|
from doreah.logging import log
|
30
|
-
from doreah.auth import authenticated_api, authenticated_api_with_alternate
|
31
30
|
import doreah
|
32
31
|
|
33
32
|
|
@@ -42,6 +41,7 @@ from collections import namedtuple
|
|
42
41
|
from threading import Lock
|
43
42
|
import yaml, json
|
44
43
|
import math
|
44
|
+
from itertools import takewhile
|
45
45
|
|
46
46
|
# url handling
|
47
47
|
import urllib
|
@@ -318,7 +318,7 @@ def associate_tracks_to_album(target_id,source_ids):
|
|
318
318
|
if target_id:
|
319
319
|
target = sqldb.get_album(target_id)
|
320
320
|
log(f"Adding {sources} into {target}")
|
321
|
-
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
|
321
|
+
sqldb.add_tracks_to_albums({src:target_id for src in source_ids},replace=True)
|
322
322
|
else:
|
323
323
|
sqldb.remove_album(source_ids)
|
324
324
|
result = {'sources':sources,'target':target}
|
@@ -444,10 +444,11 @@ def get_charts_albums(dbconn=None,resolve_ids=True,only_own_albums=False,**keys)
|
|
444
444
|
(since,to) = keys.get('timerange').timestamps()
|
445
445
|
|
446
446
|
if 'artist' in keys:
|
447
|
-
|
447
|
+
artist = sqldb.get_artist(sqldb.get_artist_id(keys['artist']))
|
448
|
+
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
449
|
if only_own_albums:
|
449
450
|
# TODO: this doesnt take associated into account and doesnt change ranks
|
450
|
-
result = [e for e in result if
|
451
|
+
result = [e for e in result if artist in (e['album']['artists'] or [])]
|
451
452
|
else:
|
452
453
|
result = sqldb.count_scrobbles_by_album(since=since,to=to,resolve_ids=resolve_ids,dbconn=dbconn)
|
453
454
|
return result
|
@@ -570,7 +571,7 @@ def get_performance(dbconn=None,**keys):
|
|
570
571
|
return results
|
571
572
|
|
572
573
|
@waitfordb
|
573
|
-
def get_top_artists(dbconn=None,**keys):
|
574
|
+
def get_top_artists(dbconn=None,compatibility=True,**keys):
|
574
575
|
|
575
576
|
separate = keys.get('separate')
|
576
577
|
|
@@ -578,42 +579,73 @@ def get_top_artists(dbconn=None,**keys):
|
|
578
579
|
results = []
|
579
580
|
|
580
581
|
for rng in rngs:
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
582
|
+
result = {'range':rng}
|
583
|
+
res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)
|
584
|
+
|
585
|
+
result['top'] = [
|
586
|
+
{'artist': r['artist'], 'scrobbles': r['scrobbles'], 'real_scrobbles':r['real_scrobbles'], 'associated_artists': sqldb.get_associated_artists(r['artist'])}
|
587
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
588
|
+
]
|
589
|
+
# for third party applications
|
590
|
+
if compatibility:
|
591
|
+
if result['top']:
|
592
|
+
result.update(result['top'][0])
|
593
|
+
else:
|
594
|
+
result.update({'artist':None,'scrobbles':0,'real_scrobbles':0})
|
595
|
+
|
596
|
+
results.append(result)
|
586
597
|
|
587
598
|
return results
|
588
599
|
|
589
600
|
|
590
601
|
@waitfordb
|
591
|
-
def get_top_tracks(dbconn=None,**keys):
|
602
|
+
def get_top_tracks(dbconn=None,compatibility=True,**keys):
|
592
603
|
|
593
604
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
594
605
|
results = []
|
595
606
|
|
596
607
|
for rng in rngs:
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
608
|
+
result = {'range':rng}
|
609
|
+
res = get_charts_tracks(timerange=rng,dbconn=dbconn)
|
610
|
+
|
611
|
+
result['top'] = [
|
612
|
+
{'track': r['track'], 'scrobbles': r['scrobbles']}
|
613
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
614
|
+
]
|
615
|
+
# for third party applications
|
616
|
+
if compatibility:
|
617
|
+
if result['top']:
|
618
|
+
result.update(result['top'][0])
|
619
|
+
else:
|
620
|
+
result.update({'track':None,'scrobbles':0})
|
621
|
+
|
622
|
+
results.append(result)
|
602
623
|
|
603
624
|
return results
|
604
625
|
|
605
626
|
@waitfordb
|
606
|
-
def get_top_albums(dbconn=None,**keys):
|
627
|
+
def get_top_albums(dbconn=None,compatibility=True,**keys):
|
607
628
|
|
608
629
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
609
630
|
results = []
|
610
631
|
|
611
632
|
for rng in rngs:
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
633
|
+
|
634
|
+
result = {'range':rng}
|
635
|
+
res = get_charts_albums(timerange=rng,dbconn=dbconn)
|
636
|
+
|
637
|
+
result['top'] = [
|
638
|
+
{'album': r['album'], 'scrobbles': r['scrobbles']}
|
639
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
640
|
+
]
|
641
|
+
# for third party applications
|
642
|
+
if compatibility:
|
643
|
+
if result['top']:
|
644
|
+
result.update(result['top'][0])
|
645
|
+
else:
|
646
|
+
result.update({'album':None,'scrobbles':0})
|
647
|
+
|
648
|
+
results.append(result)
|
617
649
|
|
618
650
|
return results
|
619
651
|
|
@@ -913,7 +945,7 @@ def start_db():
|
|
913
945
|
|
914
946
|
# inform time module about begin of scrobbling
|
915
947
|
try:
|
916
|
-
firstscrobble = sqldb.get_scrobbles()[0]
|
948
|
+
firstscrobble = sqldb.get_scrobbles(limit=1)[0]
|
917
949
|
register_scrobbletime(firstscrobble['time'])
|
918
950
|
except IndexError:
|
919
951
|
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:
|