malojaserver 3.2.2__py3-none-any.whl → 3.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maloja/__main__.py +1 -1
- maloja/__pkginfo__.py +1 -1
- maloja/apis/_base.py +26 -19
- maloja/apis/_exceptions.py +1 -1
- maloja/apis/audioscrobbler.py +35 -7
- maloja/apis/audioscrobbler_legacy.py +5 -5
- maloja/apis/listenbrainz.py +7 -5
- maloja/apis/native_v1.py +43 -26
- maloja/cleanup.py +9 -7
- maloja/data_files/config/rules/predefined/krateng_kpopgirlgroups.tsv +2 -2
- maloja/database/__init__.py +55 -23
- maloja/database/associated.py +10 -6
- maloja/database/exceptions.py +28 -3
- maloja/database/sqldb.py +216 -168
- maloja/dev/profiler.py +3 -4
- maloja/images.py +6 -0
- maloja/malojauri.py +2 -0
- maloja/pkg_global/conf.py +29 -28
- maloja/proccontrol/tasks/export.py +2 -1
- maloja/proccontrol/tasks/import_scrobbles.py +57 -15
- maloja/server.py +4 -5
- maloja/setup.py +13 -7
- maloja/web/jinja/abstracts/base.jinja +1 -1
- maloja/web/jinja/admin_albumless.jinja +2 -0
- maloja/web/jinja/admin_overview.jinja +3 -3
- maloja/web/jinja/admin_setup.jinja +1 -1
- maloja/web/jinja/partials/album_showcase.jinja +1 -1
- maloja/web/jinja/snippets/entityrow.jinja +2 -2
- maloja/web/jinja/snippets/links.jinja +3 -1
- maloja/web/static/css/maloja.css +8 -2
- maloja/web/static/css/startpage.css +2 -2
- maloja/web/static/js/manualscrobble.js +1 -1
- maloja/web/static/js/notifications.js +16 -8
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/RECORD +38 -38
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/entry_points.txt +0 -0
    
        maloja/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)",
         |