malojaserver 3.2.2__py3-none-any.whl → 3.2.3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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/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
|
248
|
-
|
249
|
-
|
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
|
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
|
273
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
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(
|
383
|
+
dbconn.execute(DB['scrobbles'].insert().values(
|
384
|
+
**scrobble_entry
|
385
|
+
))
|
348
386
|
success += 1
|
349
|
-
except sql.exc.IntegrityError
|
350
|
-
|
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
|
-
|
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:
|
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:
|
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:
|
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(
|
631
|
+
def edit_artist(artist_id: int, artistupdatedict: str, dbconn=None) -> bool:
|
586
632
|
|
587
|
-
artist = get_artist(
|
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,
|
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==
|
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(
|
654
|
+
def edit_track(track_id: int, trackupdatedict: dict, dbconn=None) -> bool:
|
609
655
|
|
610
|
-
track = get_track(
|
611
|
-
changedtrack = {**track
|
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,
|
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==
|
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(
|
677
|
+
def edit_album(album_id: int, albumupdatedict: dict, dbconn=None) -> bool:
|
632
678
|
|
633
|
-
album = get_album(
|
634
|
-
changedalbum = {**album
|
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,
|
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==
|
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
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
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(
|
1643
|
+
def get_track(track_id: int, dbconn=None) -> TrackDict:
|
1605
1644
|
op = DB['tracks'].select().where(
|
1606
|
-
DB['tracks'].c.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(
|
1655
|
+
def get_artist(artist_id: int, dbconn=None) -> str:
|
1616
1656
|
op = DB['artists'].select().where(
|
1617
|
-
DB['artists'].c.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(
|
1667
|
+
def get_album(album_id: int, dbconn=None) -> AlbumDict:
|
1627
1668
|
op = DB['albums'].select().where(
|
1628
|
-
DB['albums'].c.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)",
|