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/database/sqldb.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import TypedDict, Optional, cast
2
+
1
3
  import sqlalchemy as sql
2
4
  from sqlalchemy.dialects.sqlite import insert as sqliteinsert
3
5
  import json
@@ -213,6 +215,25 @@ def set_maloja_info(info,dbconn=None):
213
215
  # The last two fields are not returned under normal circumstances
214
216
 
215
217
 
218
+ class AlbumDict(TypedDict):
219
+ albumtitle: str
220
+ artists: list[str]
221
+
222
+
223
+ class TrackDict(TypedDict):
224
+ artists: list[str]
225
+ title: str
226
+ album: AlbumDict
227
+ length: int | None
228
+
229
+
230
+ class ScrobbleDict(TypedDict):
231
+ time: int
232
+ track: TrackDict
233
+ duration: int
234
+ origin: str
235
+ extra: Optional[dict]
236
+ rawscrobble: Optional[dict]
216
237
 
217
238
 
218
239
  ##### Conversions between DB and dicts
@@ -222,140 +243,164 @@ def set_maloja_info(info,dbconn=None):
222
243
 
223
244
 
224
245
  ### DB -> DICT
225
- def scrobbles_db_to_dict(rows,include_internal=False,dbconn=None):
226
- tracks = get_tracks_map(set(row.track_id for row in rows),dbconn=dbconn)
246
+ def scrobbles_db_to_dict(rows, include_internal=False, dbconn=None) -> list[ScrobbleDict]:
247
+ tracks: list[TrackDict] = get_tracks_map(set(row.track_id for row in rows), dbconn=dbconn)
227
248
  return [
228
- {
249
+ cast(ScrobbleDict, {
229
250
  **{
230
- "time":row.timestamp,
231
- "track":tracks[row.track_id],
232
- "duration":row.duration,
233
- "origin":row.origin,
251
+ "time": row.timestamp,
252
+ "track": tracks[row.track_id],
253
+ "duration": row.duration,
254
+ "origin": row.origin
234
255
  },
235
256
  **({
236
- "extra":json.loads(row.extra or '{}'),
237
- "rawscrobble":json.loads(row.rawscrobble or '{}')
257
+ "extra": json.loads(row.extra or '{}'),
258
+ "rawscrobble": json.loads(row.rawscrobble or '{}')
238
259
  } if include_internal else {})
239
- }
260
+ })
240
261
 
241
262
  for row in rows
242
263
  ]
243
264
 
244
- def scrobble_db_to_dict(row,dbconn=None):
245
- return scrobbles_db_to_dict([row],dbconn=dbconn)[0]
246
265
 
247
- def tracks_db_to_dict(rows,dbconn=None):
248
- artists = get_artists_of_tracks(set(row.id for row in rows),dbconn=dbconn)
249
- albums = get_albums_map(set(row.album_id for row in rows),dbconn=dbconn)
266
+ def scrobble_db_to_dict(row, dbconn=None) -> ScrobbleDict:
267
+ return scrobbles_db_to_dict([row], dbconn=dbconn)[0]
268
+
269
+
270
+ def tracks_db_to_dict(rows, dbconn=None) -> list[TrackDict]:
271
+ artists = get_artists_of_tracks(set(row.id for row in rows), dbconn=dbconn)
272
+ albums = get_albums_map(set(row.album_id for row in rows), dbconn=dbconn)
250
273
  return [
251
- {
274
+ cast(TrackDict, {
252
275
  "artists":artists[row.id],
253
276
  "title":row.title,
254
277
  "album":albums.get(row.album_id),
255
278
  "length":row.length
256
- }
279
+ })
257
280
  for row in rows
258
281
  ]
259
282
 
260
- def track_db_to_dict(row,dbconn=None):
261
- return tracks_db_to_dict([row],dbconn=dbconn)[0]
262
283
 
263
- def artists_db_to_dict(rows,dbconn=None):
284
+ def track_db_to_dict(row, dbconn=None) -> TrackDict:
285
+ return tracks_db_to_dict([row], dbconn=dbconn)[0]
286
+
287
+
288
+ def artists_db_to_dict(rows, dbconn=None) -> list[str]:
264
289
  return [
265
290
  row.name
266
291
  for row in rows
267
292
  ]
268
293
 
269
- def artist_db_to_dict(row,dbconn=None):
270
- return artists_db_to_dict([row],dbconn=dbconn)[0]
271
294
 
272
- def albums_db_to_dict(rows,dbconn=None):
273
- artists = get_artists_of_albums(set(row.id for row in rows),dbconn=dbconn)
295
+ def artist_db_to_dict(row, dbconn=None) -> str:
296
+ return artists_db_to_dict([row], dbconn=dbconn)[0]
297
+
298
+
299
+ def albums_db_to_dict(rows, dbconn=None) -> list[AlbumDict]:
300
+ artists = get_artists_of_albums(set(row.id for row in rows), dbconn=dbconn)
274
301
  return [
275
- {
276
- "artists":artists.get(row.id),
277
- "albumtitle":row.albtitle,
278
- }
302
+ cast(AlbumDict, {
303
+ "artists": artists.get(row.id),
304
+ "albumtitle": row.albtitle,
305
+ })
279
306
  for row in rows
280
307
  ]
281
308
 
282
- def album_db_to_dict(row,dbconn=None):
283
- return albums_db_to_dict([row],dbconn=dbconn)[0]
284
-
285
309
 
310
+ def album_db_to_dict(row, dbconn=None) -> AlbumDict:
311
+ return albums_db_to_dict([row], dbconn=dbconn)[0]
286
312
 
287
313
 
288
314
  ### DICT -> DB
289
315
  # These should return None when no data is in the dict so they can be used for update statements
290
316
 
291
- def scrobble_dict_to_db(info,update_album=False,dbconn=None):
317
+ def scrobble_dict_to_db(info: ScrobbleDict, update_album=False, dbconn=None):
292
318
  return {
293
- "timestamp":info.get('time'),
294
- "origin":info.get('origin'),
295
- "duration":info.get('duration'),
296
- "track_id":get_track_id(info.get('track'),update_album=update_album,dbconn=dbconn),
297
- "extra":json.dumps(info.get('extra')) if info.get('extra') else None,
298
- "rawscrobble":json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') else None
319
+ "timestamp": info.get('time'),
320
+ "origin": info.get('origin'),
321
+ "duration": info.get('duration'),
322
+ "track_id": get_track_id(info.get('track'), update_album=update_album, dbconn=dbconn),
323
+ "extra": json.dumps(info.get('extra')) if info.get('extra') else None,
324
+ "rawscrobble": json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') else None
299
325
  }
300
326
 
301
- def track_dict_to_db(info,dbconn=None):
327
+
328
+ def track_dict_to_db(info: TrackDict, dbconn=None):
302
329
  return {
303
- "title":info.get('title'),
304
- "title_normalized":normalize_name(info.get('title','')) or None,
305
- "length":info.get('length')
330
+ "title": info.get('title'),
331
+ "title_normalized": normalize_name(info.get('title', '')) or None,
332
+ "length": info.get('length')
306
333
  }
307
334
 
308
- def artist_dict_to_db(info,dbconn=None):
335
+
336
+ def artist_dict_to_db(info: str, dbconn=None):
309
337
  return {
310
338
  "name": info,
311
- "name_normalized":normalize_name(info)
339
+ "name_normalized": normalize_name(info)
312
340
  }
313
341
 
314
- def album_dict_to_db(info,dbconn=None):
342
+
343
+ def album_dict_to_db(info: AlbumDict, dbconn=None):
315
344
  return {
316
- "albtitle":info.get('albumtitle'),
317
- "albtitle_normalized":normalize_name(info.get('albumtitle'))
345
+ "albtitle": info.get('albumtitle'),
346
+ "albtitle_normalized": normalize_name(info.get('albumtitle'))
318
347
  }
319
348
 
320
349
 
321
350
 
322
351
 
323
-
324
352
  ##### Actual Database interactions
325
353
 
326
354
  # TODO: remove all resolve_id args and do that logic outside the caching to improve hit chances
327
355
  # TODO: maybe also factor out all intitial get entity funcs (some here, some in __init__) and throw exceptions
328
356
 
329
357
  @connection_provider
330
- def add_scrobble(scrobbledict,update_album=False,dbconn=None):
331
- add_scrobbles([scrobbledict],update_album=update_album,dbconn=dbconn)
358
+ def add_scrobble(scrobbledict: ScrobbleDict, update_album=False, dbconn=None):
359
+ _, ex, er = add_scrobbles([scrobbledict], update_album=update_album, dbconn=dbconn)
360
+ if er > 0:
361
+ raise exc.DuplicateTimestamp(existing_scrobble=None, rejected_scrobble=scrobbledict)
362
+ # TODO: actually pass existing scrobble
363
+ elif ex > 0:
364
+ raise exc.DuplicateScrobble(scrobble=scrobbledict)
365
+
332
366
 
333
367
  @connection_provider
334
- def add_scrobbles(scrobbleslist,update_album=False,dbconn=None):
368
+ def add_scrobbles(scrobbleslist: list[ScrobbleDict], update_album=False, dbconn=None) -> tuple[int, int, int]:
335
369
 
336
370
  with SCROBBLE_LOCK:
337
371
 
338
- ops = [
339
- DB['scrobbles'].insert().values(
340
- **scrobble_dict_to_db(s,update_album=update_album,dbconn=dbconn)
341
- ) for s in scrobbleslist
342
- ]
372
+ # ops = [
373
+ # DB['scrobbles'].insert().values(
374
+ # **scrobble_dict_to_db(s,update_album=update_album,dbconn=dbconn)
375
+ # ) for s in scrobbleslist
376
+ # ]
343
377
 
344
- success,errors = 0,0
345
- for op in ops:
378
+ success, exists, errors = 0, 0, 0
379
+
380
+ for s in scrobbleslist:
381
+ scrobble_entry = scrobble_dict_to_db(s, update_album=update_album, dbconn=dbconn)
346
382
  try:
347
- dbconn.execute(op)
383
+ dbconn.execute(DB['scrobbles'].insert().values(
384
+ **scrobble_entry
385
+ ))
348
386
  success += 1
349
- except sql.exc.IntegrityError as e:
350
- errors += 1
387
+ except sql.exc.IntegrityError:
388
+ # get existing scrobble
389
+ result = dbconn.execute(DB['scrobbles'].select().where(
390
+ DB['scrobbles'].c.timestamp == scrobble_entry['timestamp']
391
+ )).first()
392
+ if result.track_id == scrobble_entry['track_id']:
393
+ exists += 1
394
+ else:
395
+ errors += 1
351
396
 
352
- # TODO check if actual duplicate
397
+ if errors > 0: log(f"{errors} Scrobbles have not been written to database (duplicate timestamps)!", color='red')
398
+ if exists > 0: log(f"{exists} Scrobbles have not been written to database (already exist)", color='orange')
399
+ return success, exists, errors
353
400
 
354
- if errors > 0: log(f"{errors} Scrobbles have not been written to database!",color='red')
355
- return success,errors
356
401
 
357
402
  @connection_provider
358
- def delete_scrobble(scrobble_id,dbconn=None):
403
+ def delete_scrobble(scrobble_id: int, dbconn=None) -> bool:
359
404
 
360
405
  with SCROBBLE_LOCK:
361
406
 
@@ -369,7 +414,7 @@ def delete_scrobble(scrobble_id,dbconn=None):
369
414
 
370
415
 
371
416
  @connection_provider
372
- def add_track_to_album(track_id,album_id,replace=False,dbconn=None):
417
+ def add_track_to_album(track_id: int, album_id: int, replace=False, dbconn=None) -> bool:
373
418
 
374
419
  conditions = [
375
420
  DB['tracks'].c.id == track_id
@@ -398,39 +443,39 @@ def add_track_to_album(track_id,album_id,replace=False,dbconn=None):
398
443
  # ALL OF RECORDED HISTORY in order to display top weeks
399
444
  # lmao
400
445
  # TODO: figure out something better
401
-
402
-
403
446
  return True
404
447
 
448
+
405
449
  @connection_provider
406
- def add_tracks_to_albums(track_to_album_id_dict,replace=False,dbconn=None):
450
+ def add_tracks_to_albums(track_to_album_id_dict: dict[int, int], replace=False, dbconn=None) -> bool:
407
451
 
408
452
  for track_id in track_to_album_id_dict:
409
- add_track_to_album(track_id,track_to_album_id_dict[track_id],dbconn=dbconn)
453
+ add_track_to_album(track_id,track_to_album_id_dict[track_id], replace=replace, dbconn=dbconn)
454
+ return True
455
+
410
456
 
411
457
  @connection_provider
412
- def remove_album(*track_ids,dbconn=None):
458
+ def remove_album(*track_ids: list[int], dbconn=None) -> bool:
413
459
 
414
460
  DB['tracks'].update().where(
415
461
  DB['tracks'].c.track_id.in_(track_ids)
416
462
  ).values(
417
463
  album_id=None
418
464
  )
465
+ return True
466
+
419
467
 
420
468
  ### these will 'get' the ID of an entity, creating it if necessary
421
469
 
422
470
  @cached_wrapper
423
471
  @connection_provider
424
- def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
472
+ def get_track_id(trackdict: TrackDict, create_new=True, update_album=False, dbconn=None) -> int | None:
425
473
  ntitle = normalize_name(trackdict['title'])
426
- artist_ids = [get_artist_id(a,create_new=create_new,dbconn=dbconn) for a in trackdict['artists']]
474
+ artist_ids = [get_artist_id(a, create_new=create_new, dbconn=dbconn) for a in trackdict['artists']]
427
475
  artist_ids = list(set(artist_ids))
428
476
 
429
-
430
-
431
-
432
477
  op = DB['tracks'].select().where(
433
- DB['tracks'].c.title_normalized==ntitle
478
+ DB['tracks'].c.title_normalized == ntitle
434
479
  )
435
480
  result = dbconn.execute(op).all()
436
481
  for row in result:
@@ -440,7 +485,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
440
485
  op = DB['trackartists'].select(
441
486
  # DB['trackartists'].c.artist_id
442
487
  ).where(
443
- DB['trackartists'].c.track_id==row.id
488
+ DB['trackartists'].c.track_id == row.id
444
489
  )
445
490
  result = dbconn.execute(op).all()
446
491
  match_artist_ids = [r.artist_id for r in result]
@@ -456,14 +501,14 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
456
501
  album_id = get_album_id(trackdict['album'],create_new=(update_album or not row.album_id),dbconn=dbconn)
457
502
  add_track_to_album(row.id,album_id,replace=update_album,dbconn=dbconn)
458
503
 
459
-
460
504
  return row.id
461
505
 
462
- if not create_new: return None
506
+ if not create_new:
507
+ return None
463
508
 
464
509
  #print("Creating new track")
465
510
  op = DB['tracks'].insert().values(
466
- **track_dict_to_db(trackdict,dbconn=dbconn)
511
+ **track_dict_to_db(trackdict, dbconn=dbconn)
467
512
  )
468
513
  result = dbconn.execute(op)
469
514
  track_id = result.inserted_primary_key[0]
@@ -478,24 +523,26 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
478
523
  #print("Created",trackdict['title'],track_id)
479
524
 
480
525
  if trackdict.get('album'):
481
- add_track_to_album(track_id,get_album_id(trackdict['album'],dbconn=dbconn),dbconn=dbconn)
526
+ add_track_to_album(track_id, get_album_id(trackdict['album'], dbconn=dbconn), dbconn=dbconn)
482
527
  return track_id
483
528
 
529
+
484
530
  @cached_wrapper
485
531
  @connection_provider
486
- def get_artist_id(artistname,create_new=True,dbconn=None):
532
+ def get_artist_id(artistname: str, create_new=True, dbconn=None) -> int | None:
487
533
  nname = normalize_name(artistname)
488
534
  #print("looking for",nname)
489
535
 
490
536
  op = DB['artists'].select().where(
491
- DB['artists'].c.name_normalized==nname
537
+ DB['artists'].c.name_normalized == nname
492
538
  )
493
539
  result = dbconn.execute(op).all()
494
540
  for row in result:
495
541
  #print("ID for",artistname,"was",row[0])
496
542
  return row.id
497
543
 
498
- if not create_new: return None
544
+ if not create_new:
545
+ return None
499
546
 
500
547
  op = DB['artists'].insert().values(
501
548
  name=artistname,
@@ -508,15 +555,15 @@ def get_artist_id(artistname,create_new=True,dbconn=None):
508
555
 
509
556
  @cached_wrapper
510
557
  @connection_provider
511
- def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None):
558
+ def get_album_id(albumdict: AlbumDict, create_new=True, ignore_albumartists=False, dbconn=None) -> int | None:
512
559
  ntitle = normalize_name(albumdict['albumtitle'])
513
- artist_ids = [get_artist_id(a,dbconn=dbconn) for a in (albumdict.get('artists') or [])]
560
+ artist_ids = [get_artist_id(a, dbconn=dbconn) for a in (albumdict.get('artists') or [])]
514
561
  artist_ids = list(set(artist_ids))
515
562
 
516
563
  op = DB['albums'].select(
517
564
  # DB['albums'].c.id
518
565
  ).where(
519
- DB['albums'].c.albtitle_normalized==ntitle
566
+ DB['albums'].c.albtitle_normalized == ntitle
520
567
  )
521
568
  result = dbconn.execute(op).all()
522
569
  for row in result:
@@ -529,7 +576,7 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
529
576
  op = DB['albumartists'].select(
530
577
  # DB['albumartists'].c.artist_id
531
578
  ).where(
532
- DB['albumartists'].c.album_id==row.id
579
+ DB['albumartists'].c.album_id == row.id
533
580
  )
534
581
  result = dbconn.execute(op).all()
535
582
  match_artist_ids = [r.artist_id for r in result]
@@ -538,11 +585,11 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
538
585
  #print("ID for",albumdict['title'],"was",row[0])
539
586
  return row.id
540
587
 
541
- if not create_new: return None
542
-
588
+ if not create_new:
589
+ return None
543
590
 
544
591
  op = DB['albums'].insert().values(
545
- **album_dict_to_db(albumdict,dbconn=dbconn)
592
+ **album_dict_to_db(albumdict, dbconn=dbconn)
546
593
  )
547
594
  result = dbconn.execute(op)
548
595
  album_id = result.inserted_primary_key[0]
@@ -557,18 +604,15 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
557
604
  return album_id
558
605
 
559
606
 
560
-
561
-
562
607
  ### Edit existing
563
608
 
564
-
565
609
  @connection_provider
566
- def edit_scrobble(scrobble_id,scrobbleupdatedict,dbconn=None):
610
+ def edit_scrobble(scrobble_id: int, scrobbleupdatedict: dict, dbconn=None) -> bool:
567
611
 
568
612
  dbentry = scrobble_dict_to_db(scrobbleupdatedict,dbconn=dbconn)
569
- dbentry = {k:v for k,v in dbentry.items() if v}
613
+ dbentry = {k: v for k, v in dbentry.items() if v}
570
614
 
571
- print("Updating scrobble",dbentry)
615
+ print("Updating scrobble", dbentry)
572
616
 
573
617
  with SCROBBLE_LOCK:
574
618
 
@@ -579,97 +623,97 @@ def edit_scrobble(scrobble_id,scrobbleupdatedict,dbconn=None):
579
623
  )
580
624
 
581
625
  dbconn.execute(op)
626
+ return True
627
+
582
628
 
583
629
  # edit function only for primary db information (not linked fields)
584
630
  @connection_provider
585
- def edit_artist(id,artistupdatedict,dbconn=None):
631
+ def edit_artist(artist_id: int, artistupdatedict: str, dbconn=None) -> bool:
586
632
 
587
- artist = get_artist(id)
633
+ artist = get_artist(artist_id)
588
634
  changedartist = artistupdatedict # well
589
635
 
590
- dbentry = artist_dict_to_db(artistupdatedict,dbconn=dbconn)
591
- dbentry = {k:v for k,v in dbentry.items() if v}
636
+ dbentry = artist_dict_to_db(artistupdatedict, dbconn=dbconn)
637
+ dbentry = {k: v for k, v in dbentry.items() if v}
592
638
 
593
- existing_artist_id = get_artist_id(changedartist,create_new=False,dbconn=dbconn)
594
- if existing_artist_id not in (None,id):
639
+ existing_artist_id = get_artist_id(changedartist, create_new=False, dbconn=dbconn)
640
+ if existing_artist_id not in (None, artist_id):
595
641
  raise exc.ArtistExists(changedartist)
596
642
 
597
643
  op = DB['artists'].update().where(
598
- DB['artists'].c.id==id
644
+ DB['artists'].c.id == artist_id
599
645
  ).values(
600
646
  **dbentry
601
647
  )
602
648
  result = dbconn.execute(op)
603
-
604
649
  return True
605
650
 
651
+
606
652
  # edit function only for primary db information (not linked fields)
607
653
  @connection_provider
608
- def edit_track(id,trackupdatedict,dbconn=None):
654
+ def edit_track(track_id: int, trackupdatedict: dict, dbconn=None) -> bool:
609
655
 
610
- track = get_track(id,dbconn=dbconn)
611
- changedtrack = {**track,**trackupdatedict}
656
+ track = get_track(track_id, dbconn=dbconn)
657
+ changedtrack: TrackDict = {**track, **trackupdatedict}
612
658
 
613
- dbentry = track_dict_to_db(trackupdatedict,dbconn=dbconn)
614
- dbentry = {k:v for k,v in dbentry.items() if v}
659
+ dbentry = track_dict_to_db(trackupdatedict, dbconn=dbconn)
660
+ dbentry = {k: v for k, v in dbentry.items() if v}
615
661
 
616
- existing_track_id = get_track_id(changedtrack,create_new=False,dbconn=dbconn)
617
- if existing_track_id not in (None,id):
662
+ existing_track_id = get_track_id(changedtrack, create_new=False, dbconn=dbconn)
663
+ if existing_track_id not in (None, track_id):
618
664
  raise exc.TrackExists(changedtrack)
619
665
 
620
666
  op = DB['tracks'].update().where(
621
- DB['tracks'].c.id==id
667
+ DB['tracks'].c.id == track_id
622
668
  ).values(
623
669
  **dbentry
624
670
  )
625
671
  result = dbconn.execute(op)
626
-
627
672
  return True
628
673
 
674
+
629
675
  # edit function only for primary db information (not linked fields)
630
676
  @connection_provider
631
- def edit_album(id,albumupdatedict,dbconn=None):
677
+ def edit_album(album_id: int, albumupdatedict: dict, dbconn=None) -> bool:
632
678
 
633
- album = get_album(id,dbconn=dbconn)
634
- changedalbum = {**album,**albumupdatedict}
679
+ album = get_album(album_id, dbconn=dbconn)
680
+ changedalbum: AlbumDict = {**album, **albumupdatedict}
635
681
 
636
- dbentry = album_dict_to_db(albumupdatedict,dbconn=dbconn)
637
- dbentry = {k:v for k,v in dbentry.items() if v}
682
+ dbentry = album_dict_to_db(albumupdatedict, dbconn=dbconn)
683
+ dbentry = {k: v for k, v in dbentry.items() if v}
638
684
 
639
- existing_album_id = get_album_id(changedalbum,create_new=False,dbconn=dbconn)
640
- if existing_album_id not in (None,id):
685
+ existing_album_id = get_album_id(changedalbum, create_new=False, dbconn=dbconn)
686
+ if existing_album_id not in (None, album_id):
641
687
  raise exc.TrackExists(changedalbum)
642
688
 
643
689
  op = DB['albums'].update().where(
644
- DB['albums'].c.id==id
690
+ DB['albums'].c.id == album_id
645
691
  ).values(
646
692
  **dbentry
647
693
  )
648
694
  result = dbconn.execute(op)
649
-
650
695
  return True
651
696
 
652
697
 
653
698
  ### Edit associations
654
699
 
655
700
  @connection_provider
656
- def add_artists_to_tracks(track_ids,artist_ids,dbconn=None):
701
+ def add_artists_to_tracks(track_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
657
702
 
658
703
  op = DB['trackartists'].insert().values([
659
- {'track_id':track_id,'artist_id':artist_id}
704
+ {'track_id': track_id, 'artist_id': artist_id}
660
705
  for track_id in track_ids for artist_id in artist_ids
661
706
  ])
662
707
 
663
708
  result = dbconn.execute(op)
664
-
665
709
  # the resulting tracks could now be duplicates of existing ones
666
710
  # this also takes care of clean_db
667
711
  merge_duplicate_tracks(dbconn=dbconn)
668
-
669
712
  return True
670
713
 
714
+
671
715
  @connection_provider
672
- def remove_artists_from_tracks(track_ids,artist_ids,dbconn=None):
716
+ def remove_artists_from_tracks(track_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
673
717
 
674
718
  # only tracks that have at least one other artist
675
719
  subquery = DB['trackartists'].select().where(
@@ -687,16 +731,14 @@ def remove_artists_from_tracks(track_ids,artist_ids,dbconn=None):
687
731
  )
688
732
 
689
733
  result = dbconn.execute(op)
690
-
691
734
  # the resulting tracks could now be duplicates of existing ones
692
735
  # this also takes care of clean_db
693
736
  merge_duplicate_tracks(dbconn=dbconn)
694
-
695
737
  return True
696
738
 
697
739
 
698
740
  @connection_provider
699
- def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
741
+ def add_artists_to_albums(album_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
700
742
 
701
743
  op = DB['albumartists'].insert().values([
702
744
  {'album_id':album_id,'artist_id':artist_id}
@@ -704,16 +746,14 @@ def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
704
746
  ])
705
747
 
706
748
  result = dbconn.execute(op)
707
-
708
749
  # the resulting albums could now be duplicates of existing ones
709
750
  # this also takes care of clean_db
710
751
  merge_duplicate_albums(dbconn=dbconn)
711
-
712
752
  return True
713
753
 
714
754
 
715
755
  @connection_provider
716
- def remove_artists_from_albums(album_ids,artist_ids,dbconn=None):
756
+ def remove_artists_from_albums(album_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
717
757
 
718
758
  # no check here, albums are allowed to have zero artists
719
759
 
@@ -725,17 +765,16 @@ def remove_artists_from_albums(album_ids,artist_ids,dbconn=None):
725
765
  )
726
766
 
727
767
  result = dbconn.execute(op)
728
-
729
768
  # the resulting albums could now be duplicates of existing ones
730
769
  # this also takes care of clean_db
731
770
  merge_duplicate_albums(dbconn=dbconn)
732
-
733
771
  return True
734
772
 
773
+
735
774
  ### Merge
736
775
 
737
776
  @connection_provider
738
- def merge_tracks(target_id,source_ids,dbconn=None):
777
+ def merge_tracks(target_id: int, source_ids: list[int], dbconn=None) -> bool:
739
778
 
740
779
  op = DB['scrobbles'].update().where(
741
780
  DB['scrobbles'].c.track_id.in_(source_ids)
@@ -744,11 +783,11 @@ def merge_tracks(target_id,source_ids,dbconn=None):
744
783
  )
745
784
  result = dbconn.execute(op)
746
785
  clean_db(dbconn=dbconn)
747
-
748
786
  return True
749
787
 
788
+
750
789
  @connection_provider
751
- def merge_artists(target_id,source_ids,dbconn=None):
790
+ def merge_artists(target_id: int, source_ids: list[int], dbconn=None) -> bool:
752
791
 
753
792
  # some tracks could already have multiple of the to be merged artists
754
793
 
@@ -776,7 +815,6 @@ def merge_artists(target_id,source_ids,dbconn=None):
776
815
 
777
816
  result = dbconn.execute(op)
778
817
 
779
-
780
818
  # same for albums
781
819
  op = DB['albumartists'].select().where(
782
820
  DB['albumartists'].c.artist_id.in_(source_ids + [target_id])
@@ -797,7 +835,6 @@ def merge_artists(target_id,source_ids,dbconn=None):
797
835
 
798
836
  result = dbconn.execute(op)
799
837
 
800
-
801
838
  # tracks_artists = {}
802
839
  # for row in result:
803
840
  # tracks_artists.setdefault(row.track_id,[]).append(row.artist_id)
@@ -814,15 +851,14 @@ def merge_artists(target_id,source_ids,dbconn=None):
814
851
  # result = dbconn.execute(op)
815
852
 
816
853
  # this could have created duplicate tracks and albums
817
- merge_duplicate_tracks(artist_id=target_id,dbconn=dbconn)
818
- merge_duplicate_albums(artist_id=target_id,dbconn=dbconn)
854
+ merge_duplicate_tracks(artist_id=target_id, dbconn=dbconn)
855
+ merge_duplicate_albums(artist_id=target_id, dbconn=dbconn)
819
856
  clean_db(dbconn=dbconn)
820
-
821
857
  return True
822
858
 
823
859
 
824
860
  @connection_provider
825
- def merge_albums(target_id,source_ids,dbconn=None):
861
+ def merge_albums(target_id: int, source_ids: list[int], dbconn=None) -> bool:
826
862
 
827
863
  op = DB['tracks'].update().where(
828
864
  DB['tracks'].c.album_id.in_(source_ids)
@@ -831,7 +867,6 @@ def merge_albums(target_id,source_ids,dbconn=None):
831
867
  )
832
868
  result = dbconn.execute(op)
833
869
  clean_db(dbconn=dbconn)
834
-
835
870
  return True
836
871
 
837
872
 
@@ -860,19 +895,24 @@ def get_scrobbles_of_artist(artist,since=None,to=None,resolve_references=True,li
860
895
  op = op.order_by(sql.desc('timestamp'))
861
896
  else:
862
897
  op = op.order_by(sql.asc('timestamp'))
863
- if limit:
898
+ if limit and not associated:
899
+ # if we count associated we cant limit here because we remove stuff later!
864
900
  op = op.limit(limit)
865
901
  result = dbconn.execute(op).all()
866
902
 
867
903
  # remove duplicates (multiple associated artists in the song, e.g. Irene & Seulgi being both counted as Red Velvet)
868
904
  # distinct on doesn't seem to exist in sqlite
869
- seen = set()
870
- filtered_result = []
871
- for row in result:
872
- if row.timestamp not in seen:
873
- filtered_result.append(row)
874
- seen.add(row.timestamp)
875
- result = filtered_result
905
+ if associated:
906
+ seen = set()
907
+ filtered_result = []
908
+ for row in result:
909
+ if row.timestamp not in seen:
910
+ filtered_result.append(row)
911
+ seen.add(row.timestamp)
912
+ result = filtered_result
913
+ if limit:
914
+ result = result[:limit]
915
+
876
916
 
877
917
 
878
918
  if resolve_references:
@@ -962,7 +1002,6 @@ def get_scrobbles(since=None,to=None,resolve_references=True,limit=None,reverse=
962
1002
  result = scrobbles_db_to_dict(result,dbconn=dbconn)
963
1003
  #result = [scrobble_db_to_dict(row,resolve_references=resolve_references) for i,row in enumerate(result) if i<max]
964
1004
 
965
-
966
1005
  return result
967
1006
 
968
1007
 
@@ -1072,7 +1111,7 @@ def count_scrobbles_by_artist(since,to,associated=True,resolve_ids=True,dbconn=N
1072
1111
  DB['scrobbles'].c.timestamp.between(since,to)
1073
1112
  ).group_by(
1074
1113
  artistselect
1075
- ).order_by(sql.desc('count'))
1114
+ ).order_by(sql.desc('count'),sql.desc('really_by_this_artist'))
1076
1115
  result = dbconn.execute(op).all()
1077
1116
 
1078
1117
  if resolve_ids:
@@ -1601,48 +1640,52 @@ def get_credited_artists(*artists,dbconn=None):
1601
1640
 
1602
1641
  @cached_wrapper
1603
1642
  @connection_provider
1604
- def get_track(id,dbconn=None):
1643
+ def get_track(track_id: int, dbconn=None) -> TrackDict:
1605
1644
  op = DB['tracks'].select().where(
1606
- DB['tracks'].c.id==id
1645
+ DB['tracks'].c.id == track_id
1607
1646
  )
1608
1647
  result = dbconn.execute(op).all()
1609
1648
 
1610
1649
  trackinfo = result[0]
1611
- return track_db_to_dict(trackinfo,dbconn=dbconn)
1650
+ return track_db_to_dict(trackinfo, dbconn=dbconn)
1651
+
1612
1652
 
1613
1653
  @cached_wrapper
1614
1654
  @connection_provider
1615
- def get_artist(id,dbconn=None):
1655
+ def get_artist(artist_id: int, dbconn=None) -> str:
1616
1656
  op = DB['artists'].select().where(
1617
- DB['artists'].c.id==id
1657
+ DB['artists'].c.id == artist_id
1618
1658
  )
1619
1659
  result = dbconn.execute(op).all()
1620
1660
 
1621
1661
  artistinfo = result[0]
1622
- return artist_db_to_dict(artistinfo,dbconn=dbconn)
1662
+ return artist_db_to_dict(artistinfo, dbconn=dbconn)
1663
+
1623
1664
 
1624
1665
  @cached_wrapper
1625
1666
  @connection_provider
1626
- def get_album(id,dbconn=None):
1667
+ def get_album(album_id: int, dbconn=None) -> AlbumDict:
1627
1668
  op = DB['albums'].select().where(
1628
- DB['albums'].c.id==id
1669
+ DB['albums'].c.id == album_id
1629
1670
  )
1630
1671
  result = dbconn.execute(op).all()
1631
1672
 
1632
1673
  albuminfo = result[0]
1633
- return album_db_to_dict(albuminfo,dbconn=dbconn)
1674
+ return album_db_to_dict(albuminfo, dbconn=dbconn)
1675
+
1634
1676
 
1635
1677
  @cached_wrapper
1636
1678
  @connection_provider
1637
- def get_scrobble(timestamp, include_internal=False, dbconn=None):
1679
+ def get_scrobble(timestamp: int, include_internal=False, dbconn=None) -> ScrobbleDict:
1638
1680
  op = DB['scrobbles'].select().where(
1639
- DB['scrobbles'].c.timestamp==timestamp
1681
+ DB['scrobbles'].c.timestamp == timestamp
1640
1682
  )
1641
1683
  result = dbconn.execute(op).all()
1642
1684
 
1643
1685
  scrobble = result[0]
1644
1686
  return scrobbles_db_to_dict(rows=[scrobble], include_internal=include_internal)[0]
1645
1687
 
1688
+
1646
1689
  @cached_wrapper
1647
1690
  @connection_provider
1648
1691
  def search_artist(searchterm,dbconn=None):
@@ -1684,6 +1727,11 @@ def clean_db(dbconn=None):
1684
1727
  log(f"Database Cleanup...")
1685
1728
 
1686
1729
  to_delete = [
1730
+ # NULL associations
1731
+ "from albumartists where album_id is NULL",
1732
+ "from albumartists where artist_id is NULL",
1733
+ "from trackartists where track_id is NULL",
1734
+ "from trackartists where artist_id is NULL",
1687
1735
  # tracks with no scrobbles (trackartist entries first)
1688
1736
  "from trackartists where track_id in (select id from tracks where id not in (select track_id from scrobbles))",
1689
1737
  "from tracks where id not in (select track_id from scrobbles)",