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.
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/__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']("Data Directory: "),conf.dir_settings['state'])
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
@@ -4,7 +4,7 @@
4
4
  # you know what f*ck it
5
5
  # this is hardcoded for now because of that damn project / package name discrepancy
6
6
  # i'll fix it one day
7
- VERSION = "3.2.2"
7
+ VERSION = "3.2.3"
8
8
  HOMEPAGE = "https://github.com/krateng/maloja"
9
9
 
10
10
 
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
- exceptiontype = sys.exc_info()[0]
67
- if exceptiontype in self.errors:
68
- response.status,result = self.errors[exceptiontype]
69
- log(f"Error with {self.__apiname__} API: {exceptiontype} (Request: {path})")
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
- response.status,result = 500,{"status":"Unknown error","code":500}
72
- log(f"Unhandled Exception with {self.__apiname__} API: {exceptiontype} (Request: {path})")
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 Exception:
86
- log("Could not find a handler for method " + str(methodname) + " in API " + self.__apiname__,module="debug")
87
- log("Keys: " + str(keys),module="debug")
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
- try:
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)
@@ -3,4 +3,4 @@ class InvalidAuthException(Exception): pass
3
3
  class InvalidMethodException(Exception): pass
4
4
  class InvalidSessionKey(Exception): pass
5
5
  class MalformedJSONException(Exception): pass
6
- class ScrobblingException(Exception): pass
6
+
@@ -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
- ScrobblingException:(500,{"error":8,"message":"Operation failed"})
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("<", "&lt;")
35
+ str_xml = str_xml.replace("<", "&lt;")
36
+ str_xml = str_xml.replace("\"", "&quot;")
37
+ str_xml = str_xml.replace("'", "&apos;")
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
- return 200,{"session":{"key":sessionkey}}
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
- return 200,{"session":{"key":sessionkey}}
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
- ScrobblingException:(500,"FAILED\n")
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):
@@ -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
- ScrobblingException:(500,{"code":500,"error":"Unspecified server error."})
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
- for f in os.listdir(data_dir["rules"]()):
19
- if f.split('.')[-1].lower() != 'tsv': continue
20
- filepath = data_dir["rules"](f)
21
- with open(filepath,'r') as filed:
22
- reader = csv.reader(filed,delimiter="\t")
23
- rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
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 Girls' Generation-TTS TaeTiSeo
164
- countas TaeTiSeo Girls' Generation
163
+ replaceartist Girls' Generation-TTS TaeTiSeo
164
+ countas TaeTiSeo Girls' Generation
165
165
 
166
166
  # Apink
167
167
  replaceartist A Pink Apink
@@ -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
- result = sqldb.count_scrobbles_by_album_combined(since=since,to=to,artist=keys['artist'],associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
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 keys['artist'] in (e['album']['artists'] or [])]
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
- try:
582
- res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)[0]
583
- results.append({"range":rng,"artist":res["artist"],"scrobbles":res["scrobbles"],"real_scrobbles":res["real_scrobbles"],"associated_artists":sqldb.get_associated_artists(res["artist"])})
584
- except Exception:
585
- results.append({"range":rng,"artist":None,"scrobbles":0,"real_scrobbles":0})
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
- try:
598
- res = get_charts_tracks(timerange=rng,dbconn=dbconn)[0]
599
- results.append({"range":rng,"track":res["track"],"scrobbles":res["scrobbles"]})
600
- except Exception:
601
- results.append({"range":rng,"track":None,"scrobbles":0})
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
- try:
613
- res = get_charts_albums(timerange=rng,dbconn=dbconn)[0]
614
- results.append({"range":rng,"album":res["album"],"scrobbles":res["scrobbles"]})
615
- except Exception:
616
- results.append({"range":rng,"album":None,"scrobbles":0})
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()))
@@ -19,12 +19,16 @@ def load_associated_rules():
19
19
 
20
20
  # load from file
21
21
  rawrules = []
22
- for f in os.listdir(data_dir["rules"]()):
23
- if f.split('.')[-1].lower() != 'tsv': continue
24
- filepath = data_dir["rules"](f)
25
- with open(filepath,'r') as filed:
26
- reader = csv.reader(filed,delimiter="\t")
27
- rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
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: