Merge pull request #324 from kettui/fix/artist-discography-completion-refactoring
Refactor artist discography completion metadata flow
This commit is contained in:
commit
12837e96d3
4 changed files with 571 additions and 332 deletions
|
|
@ -294,6 +294,7 @@ def _build_discography_release_dict(release: Any, artist_id: str) -> Optional[Di
|
||||||
return {
|
return {
|
||||||
'id': release_id,
|
'id': release_id,
|
||||||
'name': _extract_lookup_value(release, 'name', 'title', default=release_id),
|
'name': _extract_lookup_value(release, 'name', 'title', default=release_id),
|
||||||
|
'artist_name': _extract_release_artist_name(release),
|
||||||
'release_date': release_date,
|
'release_date': release_date,
|
||||||
'album_type': album_type,
|
'album_type': album_type,
|
||||||
'image_url': _extract_lookup_value(release, 'image_url', 'thumb_url', 'cover_image'),
|
'image_url': _extract_lookup_value(release, 'image_url', 'thumb_url', 'cover_image'),
|
||||||
|
|
@ -302,6 +303,34 @@ def _build_discography_release_dict(release: Any, artist_id: str) -> Optional[Di
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_release_artist_name(release: Any) -> str:
|
||||||
|
artist_name = _extract_lookup_value(release, 'artist_name', 'artist', default='') or ''
|
||||||
|
artist_name = str(artist_name).strip()
|
||||||
|
if artist_name:
|
||||||
|
return artist_name
|
||||||
|
|
||||||
|
artists = _extract_lookup_value(release, 'artists', default=[]) or []
|
||||||
|
if isinstance(artists, (str, bytes)):
|
||||||
|
return str(artists).strip()
|
||||||
|
if isinstance(artists, dict):
|
||||||
|
return str(_extract_lookup_value(artists, 'name', 'artist_name', 'title', default='') or '').strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
artists = list(artists)
|
||||||
|
except TypeError:
|
||||||
|
artists = [artists]
|
||||||
|
|
||||||
|
if not artists:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
first_artist = artists[0]
|
||||||
|
inferred_name = _extract_lookup_value(first_artist, 'name', 'artist_name', 'title')
|
||||||
|
if not inferred_name and isinstance(first_artist, str):
|
||||||
|
inferred_name = first_artist
|
||||||
|
|
||||||
|
return str(inferred_name).strip() if inferred_name else ''
|
||||||
|
|
||||||
|
|
||||||
def _sort_discography_releases(releases: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _sort_discography_releases(releases: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
def get_release_year(item):
|
def get_release_year(item):
|
||||||
if item.get('release_date'):
|
if item.get('release_date'):
|
||||||
|
|
@ -394,6 +423,387 @@ def get_artist_discography(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_completion_source_chain(source_override: Optional[str] = None) -> List[str]:
|
||||||
|
primary_source = get_primary_source()
|
||||||
|
source_chain = list(get_source_priority(primary_source))
|
||||||
|
|
||||||
|
override = (source_override or '').strip().lower()
|
||||||
|
if override:
|
||||||
|
source_chain = [override] + [source for source in source_chain if source != override]
|
||||||
|
|
||||||
|
return source_chain
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_track_items(api_tracks: Any) -> List[Dict[str, Any]]:
|
||||||
|
if not api_tracks:
|
||||||
|
return []
|
||||||
|
if isinstance(api_tracks, dict):
|
||||||
|
return api_tracks.get('items') or []
|
||||||
|
if isinstance(api_tracks, list):
|
||||||
|
return api_tracks
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_completion_artist_name(
|
||||||
|
discography: Dict[str, Any],
|
||||||
|
artist_name: str,
|
||||||
|
) -> str:
|
||||||
|
resolved_name = (artist_name or '').strip()
|
||||||
|
if resolved_name and resolved_name.lower() != 'unknown artist':
|
||||||
|
return resolved_name
|
||||||
|
|
||||||
|
release_items = list((discography or {}).get('albums', []) or []) + list((discography or {}).get('singles', []) or [])
|
||||||
|
if not release_items:
|
||||||
|
return resolved_name or 'Unknown Artist'
|
||||||
|
|
||||||
|
release_artist_name = _extract_release_artist_name(release_items[0])
|
||||||
|
if release_artist_name:
|
||||||
|
logger.debug("Using release artist metadata '%s' for completion", release_artist_name)
|
||||||
|
return release_artist_name
|
||||||
|
|
||||||
|
return resolved_name or 'Unknown Artist'
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_completion_track_total(release: Dict[str, Any], source_chain: List[str]) -> int:
|
||||||
|
total_tracks = _extract_lookup_value(release, 'total_tracks', default=0) or 0
|
||||||
|
if total_tracks:
|
||||||
|
return int(total_tracks)
|
||||||
|
|
||||||
|
release_id = _extract_lookup_value(release, 'id', 'album_id', 'release_id')
|
||||||
|
if not release_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for source in source_chain:
|
||||||
|
try:
|
||||||
|
api_tracks = get_album_tracks_for_source(source, str(release_id))
|
||||||
|
items = _extract_track_items(api_tracks)
|
||||||
|
if items:
|
||||||
|
logger.debug("Resolved track count for release %s from %s", release_id, source)
|
||||||
|
return len(items)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Could not resolve track count for release %s from %s: %s", release_id, source, exc)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def check_album_completion(
|
||||||
|
db,
|
||||||
|
album_data: Dict[str, Any],
|
||||||
|
artist_name: str,
|
||||||
|
source_override: Optional[str] = None,
|
||||||
|
source_chain: Optional[List[str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Check completion status for a single album."""
|
||||||
|
try:
|
||||||
|
source_chain = source_chain or _get_completion_source_chain(source_override)
|
||||||
|
album_name = album_data.get('name', '')
|
||||||
|
total_tracks = _resolve_completion_track_total(album_data, source_chain)
|
||||||
|
album_id = album_data.get('id', '')
|
||||||
|
|
||||||
|
# If total_tracks is 0 (Discogs masters don't include track counts),
|
||||||
|
# try to fetch the real count from the prioritized metadata sources.
|
||||||
|
if total_tracks == 0 and album_id:
|
||||||
|
logger.debug("No track count found for '%s' (%s)", album_name, album_id)
|
||||||
|
|
||||||
|
print(f"Checking album: '{album_name}' ({total_tracks} tracks)")
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
# Check if album exists in database with completeness info
|
||||||
|
try:
|
||||||
|
from config.settings import config_manager
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
||||||
|
title=album_name,
|
||||||
|
artist=artist_name,
|
||||||
|
expected_track_count=total_tracks if total_tracks > 0 else None,
|
||||||
|
confidence_threshold=0.7,
|
||||||
|
server_source=active_server
|
||||||
|
)
|
||||||
|
except Exception as db_error:
|
||||||
|
print(f"Database error for album '{album_name}': {db_error}")
|
||||||
|
return {
|
||||||
|
"id": album_id,
|
||||||
|
"name": album_name,
|
||||||
|
"status": "error",
|
||||||
|
"owned_tracks": 0,
|
||||||
|
"expected_tracks": total_tracks,
|
||||||
|
"completion_percentage": 0,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"found_in_db": False,
|
||||||
|
"error_message": str(db_error),
|
||||||
|
"formats": []
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected_tracks > 0:
|
||||||
|
completion_percentage = (owned_tracks / expected_tracks) * 100
|
||||||
|
elif total_tracks > 0:
|
||||||
|
completion_percentage = (owned_tracks / total_tracks) * 100
|
||||||
|
else:
|
||||||
|
completion_percentage = 100 if owned_tracks > 0 else 0
|
||||||
|
|
||||||
|
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
||||||
|
status = "completed"
|
||||||
|
elif owned_tracks > 0:
|
||||||
|
status = "partial"
|
||||||
|
else:
|
||||||
|
status = "missing"
|
||||||
|
|
||||||
|
print(f" Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": album_id,
|
||||||
|
"name": album_name,
|
||||||
|
"status": status,
|
||||||
|
"owned_tracks": owned_tracks,
|
||||||
|
"expected_tracks": expected_tracks or total_tracks,
|
||||||
|
"completion_percentage": round(completion_percentage, 1),
|
||||||
|
"confidence": round(confidence, 2) if confidence else 0.0,
|
||||||
|
"found_in_db": db_album is not None,
|
||||||
|
"formats": formats
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
|
||||||
|
return {
|
||||||
|
"id": album_data.get('id', ''),
|
||||||
|
"name": album_data.get('name', 'Unknown'),
|
||||||
|
"status": "error",
|
||||||
|
"owned_tracks": 0,
|
||||||
|
"expected_tracks": album_data.get('total_tracks', 0),
|
||||||
|
"completion_percentage": 0,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"found_in_db": False,
|
||||||
|
"formats": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_single_completion(
|
||||||
|
db,
|
||||||
|
single_data: Dict[str, Any],
|
||||||
|
artist_name: str,
|
||||||
|
source_override: Optional[str] = None,
|
||||||
|
source_chain: Optional[List[str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Check completion status for a single/EP."""
|
||||||
|
try:
|
||||||
|
source_chain = source_chain or _get_completion_source_chain(source_override)
|
||||||
|
single_name = single_data.get('name', '')
|
||||||
|
raw_total_tracks = single_data.get('total_tracks', 1)
|
||||||
|
total_tracks = raw_total_tracks if raw_total_tracks is not None else 1
|
||||||
|
single_id = single_data.get('id', '')
|
||||||
|
album_type = single_data.get('album_type', 'single')
|
||||||
|
formats = []
|
||||||
|
|
||||||
|
if total_tracks == 0:
|
||||||
|
total_tracks = _resolve_completion_track_total(single_data, source_chain) or 1
|
||||||
|
|
||||||
|
print(f"Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
|
||||||
|
|
||||||
|
if album_type == 'ep' or total_tracks > 1:
|
||||||
|
try:
|
||||||
|
from config.settings import config_manager
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
||||||
|
title=single_name,
|
||||||
|
artist=artist_name,
|
||||||
|
expected_track_count=total_tracks,
|
||||||
|
confidence_threshold=0.7,
|
||||||
|
server_source=active_server
|
||||||
|
)
|
||||||
|
except Exception as db_error:
|
||||||
|
print(f"Database error for EP '{single_name}': {db_error}")
|
||||||
|
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
|
||||||
|
db_album = None
|
||||||
|
|
||||||
|
if expected_tracks > 0:
|
||||||
|
completion_percentage = (owned_tracks / expected_tracks) * 100
|
||||||
|
else:
|
||||||
|
completion_percentage = (owned_tracks / total_tracks) * 100
|
||||||
|
|
||||||
|
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
||||||
|
status = "completed"
|
||||||
|
elif owned_tracks > 0:
|
||||||
|
status = "partial"
|
||||||
|
else:
|
||||||
|
status = "missing"
|
||||||
|
|
||||||
|
print(f" EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": single_id,
|
||||||
|
"name": single_name,
|
||||||
|
"status": status,
|
||||||
|
"owned_tracks": owned_tracks,
|
||||||
|
"expected_tracks": expected_tracks or total_tracks,
|
||||||
|
"completion_percentage": round(completion_percentage, 1),
|
||||||
|
"confidence": round(confidence, 2) if confidence else 0.0,
|
||||||
|
"found_in_db": db_album is not None,
|
||||||
|
"type": album_type,
|
||||||
|
"formats": formats
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from config.settings import config_manager
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
db_track, confidence = db.check_track_exists(
|
||||||
|
title=single_name,
|
||||||
|
artist=artist_name,
|
||||||
|
confidence_threshold=0.7,
|
||||||
|
server_source=active_server
|
||||||
|
)
|
||||||
|
except Exception as db_error:
|
||||||
|
print(f"Database error for single '{single_name}': {db_error}")
|
||||||
|
db_track, confidence = None, 0.0
|
||||||
|
|
||||||
|
owned_tracks = 1 if db_track else 0
|
||||||
|
expected_tracks = 1
|
||||||
|
completion_percentage = 100 if db_track else 0
|
||||||
|
status = "completed" if db_track else "missing"
|
||||||
|
|
||||||
|
if db_track and db_track.file_path:
|
||||||
|
import os
|
||||||
|
ext = os.path.splitext(db_track.file_path)[1].lstrip('.').upper()
|
||||||
|
if ext == 'MP3' and db_track.bitrate:
|
||||||
|
formats = [f"MP3-{db_track.bitrate}"]
|
||||||
|
elif ext:
|
||||||
|
formats = [ext]
|
||||||
|
|
||||||
|
print(f" Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": single_id,
|
||||||
|
"name": single_name,
|
||||||
|
"status": status,
|
||||||
|
"owned_tracks": owned_tracks,
|
||||||
|
"expected_tracks": expected_tracks,
|
||||||
|
"completion_percentage": round(completion_percentage, 1),
|
||||||
|
"confidence": round(confidence, 2) if confidence else 0.0,
|
||||||
|
"found_in_db": db_track is not None,
|
||||||
|
"type": album_type,
|
||||||
|
"formats": formats
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
|
||||||
|
return {
|
||||||
|
"id": single_data.get('id', ''),
|
||||||
|
"name": single_data.get('name', 'Unknown'),
|
||||||
|
"status": "error",
|
||||||
|
"owned_tracks": 0,
|
||||||
|
"expected_tracks": single_data.get('total_tracks', 1),
|
||||||
|
"completion_percentage": 0,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"found_in_db": False,
|
||||||
|
"type": single_data.get('album_type', 'single'),
|
||||||
|
"formats": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def iter_artist_discography_completion_events(
|
||||||
|
discography: Dict[str, Any],
|
||||||
|
artist_name: str = 'Unknown Artist',
|
||||||
|
source_override: Optional[str] = None,
|
||||||
|
db=None,
|
||||||
|
):
|
||||||
|
"""Yield completion-stream events for artist discography ownership checks."""
|
||||||
|
if db is None:
|
||||||
|
from database.music_database import get_database
|
||||||
|
|
||||||
|
db = get_database()
|
||||||
|
source_chain = _get_completion_source_chain(source_override)
|
||||||
|
resolved_artist_name = _resolve_completion_artist_name(discography or {}, artist_name)
|
||||||
|
|
||||||
|
albums = list((discography or {}).get('albums', []) or [])
|
||||||
|
singles = list((discography or {}).get('singles', []) or [])
|
||||||
|
total_items = len(albums) + len(singles)
|
||||||
|
processed_count = 0
|
||||||
|
|
||||||
|
yield {
|
||||||
|
'type': 'start',
|
||||||
|
'total_items': total_items,
|
||||||
|
'artist_name': resolved_artist_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
for album in albums:
|
||||||
|
try:
|
||||||
|
completion_data = check_album_completion(
|
||||||
|
db,
|
||||||
|
album,
|
||||||
|
resolved_artist_name,
|
||||||
|
source_override=source_override,
|
||||||
|
source_chain=source_chain,
|
||||||
|
)
|
||||||
|
completion_data['type'] = 'album_completion'
|
||||||
|
completion_data['container_type'] = 'albums'
|
||||||
|
processed_count += 1
|
||||||
|
completion_data['progress'] = round((processed_count / total_items) * 100, 1) if total_items else 100
|
||||||
|
yield completion_data
|
||||||
|
except Exception as e:
|
||||||
|
yield {
|
||||||
|
'type': 'error',
|
||||||
|
'container_type': 'albums',
|
||||||
|
'id': album.get('id', ''),
|
||||||
|
'name': album.get('name', 'Unknown'),
|
||||||
|
'error': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
for single in singles:
|
||||||
|
try:
|
||||||
|
completion_data = check_single_completion(
|
||||||
|
db,
|
||||||
|
single,
|
||||||
|
resolved_artist_name,
|
||||||
|
source_override=source_override,
|
||||||
|
source_chain=source_chain,
|
||||||
|
)
|
||||||
|
completion_data['type'] = 'single_completion'
|
||||||
|
completion_data['container_type'] = 'singles'
|
||||||
|
processed_count += 1
|
||||||
|
completion_data['progress'] = round((processed_count / total_items) * 100, 1) if total_items else 100
|
||||||
|
yield completion_data
|
||||||
|
except Exception as e:
|
||||||
|
yield {
|
||||||
|
'type': 'error',
|
||||||
|
'container_type': 'singles',
|
||||||
|
'id': single.get('id', ''),
|
||||||
|
'name': single.get('name', 'Unknown'),
|
||||||
|
'error': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
'type': 'complete',
|
||||||
|
'processed_count': processed_count,
|
||||||
|
'artist_name': resolved_artist_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_artist_discography_completion(
|
||||||
|
discography: Dict[str, Any],
|
||||||
|
artist_name: str = 'Unknown Artist',
|
||||||
|
source_override: Optional[str] = None,
|
||||||
|
db=None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Return completion results for an artist discography without streaming."""
|
||||||
|
albums_completion = []
|
||||||
|
singles_completion = []
|
||||||
|
|
||||||
|
for event in iter_artist_discography_completion_events(
|
||||||
|
discography,
|
||||||
|
artist_name=artist_name,
|
||||||
|
source_override=source_override,
|
||||||
|
db=db,
|
||||||
|
):
|
||||||
|
if event.get('type') == 'album_completion':
|
||||||
|
albums_completion.append(event)
|
||||||
|
elif event.get('type') == 'single_completion':
|
||||||
|
singles_completion.append(event)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'albums': albums_completion,
|
||||||
|
'singles': singles_completion,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_deezer_client():
|
def get_deezer_client():
|
||||||
"""Get cached Deezer client.
|
"""Get cached Deezer client.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ if "config.settings" not in sys.modules:
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
def get_active_media_server(self):
|
||||||
|
return "primary"
|
||||||
|
|
||||||
settings_mod.config_manager = _DummyConfigManager()
|
settings_mod.config_manager = _DummyConfigManager()
|
||||||
config_pkg.settings = settings_mod
|
config_pkg.settings = settings_mod
|
||||||
sys.modules["config"] = config_pkg
|
sys.modules["config"] = config_pkg
|
||||||
|
|
@ -56,6 +59,7 @@ class _FakeSourceClient:
|
||||||
self.album_calls = []
|
self.album_calls = []
|
||||||
self.artist_search_calls = []
|
self.artist_search_calls = []
|
||||||
self.discography_calls = []
|
self.discography_calls = []
|
||||||
|
self.track_search_calls = []
|
||||||
|
|
||||||
def get_artist_albums(self, artist_id, **kwargs):
|
def get_artist_albums(self, artist_id, **kwargs):
|
||||||
self.album_calls.append((artist_id, dict(kwargs)))
|
self.album_calls.append((artist_id, dict(kwargs)))
|
||||||
|
|
@ -69,6 +73,14 @@ class _FakeSourceClient:
|
||||||
self.discography_calls.append((query, dict(kwargs)))
|
self.discography_calls.append((query, dict(kwargs)))
|
||||||
return list(self.discography_results)
|
return list(self.discography_results)
|
||||||
|
|
||||||
|
def search_tracks(self, query, **kwargs):
|
||||||
|
self.track_search_calls.append((query, dict(kwargs)))
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_album_tracks(self, album_id, **kwargs):
|
||||||
|
self.album_calls.append((album_id, dict(kwargs)))
|
||||||
|
return {"items": list(self.album_results)}
|
||||||
|
|
||||||
|
|
||||||
def _album(album_id, name, release_date, album_type="album"):
|
def _album(album_id, name, release_date, album_type="album"):
|
||||||
return types.SimpleNamespace(
|
return types.SimpleNamespace(
|
||||||
|
|
@ -248,3 +260,126 @@ def test_get_artist_discography_uses_hydrabase_fast_path_when_active(monkeypatch
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
assert hydrabase.artist_search_calls == [("Artist One", {"limit": 5})]
|
assert hydrabase.artist_search_calls == [("Artist One", {"limit": 5})]
|
||||||
|
|
||||||
|
|
||||||
|
class _CompletionFakeDB:
|
||||||
|
def __init__(self, owned_tracks=1, expected_tracks=3, is_track=False):
|
||||||
|
self.owned_tracks = owned_tracks
|
||||||
|
self.expected_tracks = expected_tracks
|
||||||
|
self.is_track = is_track
|
||||||
|
self.album_calls = []
|
||||||
|
self.track_calls = []
|
||||||
|
|
||||||
|
def check_album_exists_with_completeness(self, **kwargs):
|
||||||
|
self.album_calls.append(dict(kwargs))
|
||||||
|
return (True, 0.9, self.owned_tracks, self.expected_tracks, self.owned_tracks >= self.expected_tracks, [])
|
||||||
|
|
||||||
|
def check_track_exists(self, **kwargs):
|
||||||
|
self.track_calls.append(dict(kwargs))
|
||||||
|
if self.is_track:
|
||||||
|
return (object(), 0.9)
|
||||||
|
return (None, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_iter_artist_discography_completion_uses_primary_source_first(monkeypatch):
|
||||||
|
deezer = _FakeSourceClient()
|
||||||
|
spotify = _FakeSourceClient()
|
||||||
|
itunes = _FakeSourceClient()
|
||||||
|
|
||||||
|
deezer.album_results = [{"id": "release-1-track-1"}, {"id": "release-1-track-2"}]
|
||||||
|
spotify.album_results = [{"id": "release-1-track-1"}, {"id": "release-1-track-2"}, {"id": "release-1-track-3"}]
|
||||||
|
itunes.album_results = [{"id": "release-1-track-1"}]
|
||||||
|
|
||||||
|
clients = {
|
||||||
|
"deezer": deezer,
|
||||||
|
"spotify": spotify,
|
||||||
|
"itunes": itunes,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
|
||||||
|
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])
|
||||||
|
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source: clients.get(source))
|
||||||
|
|
||||||
|
db = _CompletionFakeDB(owned_tracks=1, expected_tracks=2)
|
||||||
|
events = list(metadata_service.iter_artist_discography_completion_events(
|
||||||
|
{
|
||||||
|
"albums": [{"id": "release-1", "name": "Album One", "total_tracks": 0}],
|
||||||
|
"singles": [],
|
||||||
|
},
|
||||||
|
artist_name="Artist One",
|
||||||
|
db=db,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert events[0]["type"] == "start"
|
||||||
|
assert events[-1]["type"] == "complete"
|
||||||
|
assert events[1]["expected_tracks"] == 2
|
||||||
|
assert events[1]["status"] == "partial"
|
||||||
|
assert deezer.album_calls == [("release-1", {})]
|
||||||
|
assert spotify.album_calls == []
|
||||||
|
assert itunes.album_calls == []
|
||||||
|
assert db.album_calls and db.album_calls[0]["expected_track_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_iter_artist_discography_completion_respects_source_override(monkeypatch):
|
||||||
|
deezer = _FakeSourceClient()
|
||||||
|
spotify = _FakeSourceClient()
|
||||||
|
itunes = _FakeSourceClient()
|
||||||
|
|
||||||
|
deezer.album_results = [{"id": "release-2-track-1"}]
|
||||||
|
spotify.album_results = [{"id": "release-2-track-1"}, {"id": "release-2-track-2"}]
|
||||||
|
itunes.album_results = [{"id": "release-2-track-1"}, {"id": "release-2-track-2"}, {"id": "release-2-track-3"}]
|
||||||
|
|
||||||
|
clients = {
|
||||||
|
"deezer": deezer,
|
||||||
|
"spotify": spotify,
|
||||||
|
"itunes": itunes,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
|
||||||
|
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary, "spotify", "itunes"])
|
||||||
|
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source: clients.get(source))
|
||||||
|
|
||||||
|
db = _CompletionFakeDB(owned_tracks=1, expected_tracks=3)
|
||||||
|
events = list(metadata_service.iter_artist_discography_completion_events(
|
||||||
|
{
|
||||||
|
"albums": [{"id": "release-2", "name": "Album Two", "total_tracks": 0}],
|
||||||
|
"singles": [],
|
||||||
|
},
|
||||||
|
artist_name="Artist Two",
|
||||||
|
source_override="itunes",
|
||||||
|
db=db,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert events[1]["expected_tracks"] == 3
|
||||||
|
assert itunes.album_calls == [("release-2", {})]
|
||||||
|
assert deezer.album_calls == []
|
||||||
|
assert spotify.album_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_iter_artist_discography_completion_uses_release_artist_metadata(monkeypatch):
|
||||||
|
source = _FakeSourceClient()
|
||||||
|
clients = {"deezer": source}
|
||||||
|
|
||||||
|
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
|
||||||
|
monkeypatch.setattr(metadata_service, "get_source_priority", lambda primary: [primary])
|
||||||
|
monkeypatch.setattr(metadata_service, "get_client_for_source", lambda source_name: clients.get(source_name))
|
||||||
|
|
||||||
|
db = _CompletionFakeDB(owned_tracks=1, expected_tracks=2)
|
||||||
|
events = list(metadata_service.iter_artist_discography_completion_events(
|
||||||
|
{
|
||||||
|
"albums": [{
|
||||||
|
"id": "release-3",
|
||||||
|
"name": "Album Three",
|
||||||
|
"artist_name": "Explicit Artist",
|
||||||
|
"total_tracks": 2,
|
||||||
|
}],
|
||||||
|
"singles": [],
|
||||||
|
},
|
||||||
|
artist_name="Unknown Artist",
|
||||||
|
db=db,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert events[0]["artist_name"] == "Explicit Artist"
|
||||||
|
assert events[1]["name"] == "Album Three"
|
||||||
|
assert db.album_calls[0]["artist"] == "Explicit Artist"
|
||||||
|
assert source.track_search_calls == []
|
||||||
|
|
|
||||||
353
web_server.py
353
web_server.py
|
|
@ -11701,276 +11701,22 @@ def check_artist_discography_completion(artist_id):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'discography' not in data:
|
if not data or 'discography' not in data:
|
||||||
return jsonify({"error": "Missing discography data"}), 400
|
return jsonify({"error": "Missing discography data"}), 400
|
||||||
|
from core.metadata_service import check_artist_discography_completion as _check_artist_discography_completion
|
||||||
|
|
||||||
discography = data['discography']
|
discography = data['discography']
|
||||||
test_mode = data.get('test_mode', False) # Add test mode for demonstration
|
source_override = (data.get('source') or '').strip().lower() or None
|
||||||
albums_completion = []
|
result = _check_artist_discography_completion(
|
||||||
singles_completion = []
|
discography,
|
||||||
|
artist_name=data.get('artist_name', 'Unknown Artist'),
|
||||||
# Get database instance
|
source_override=source_override,
|
||||||
from database.music_database import MusicDatabase
|
)
|
||||||
db = MusicDatabase()
|
return jsonify(result)
|
||||||
|
|
||||||
# Get artist name - should be provided by the frontend
|
|
||||||
artist_name = data.get('artist_name', 'Unknown Artist')
|
|
||||||
|
|
||||||
# If no artist name provided, try to infer it from the request
|
|
||||||
if artist_name == 'Unknown Artist':
|
|
||||||
print(f"No artist name provided in request, attempting to infer from discography data")
|
|
||||||
# Try to extract from first album's title by using a simple search
|
|
||||||
all_items = discography.get('albums', []) + discography.get('singles', [])
|
|
||||||
if all_items and spotify_client and spotify_client.is_authenticated():
|
|
||||||
try:
|
|
||||||
first_item = all_items[0]
|
|
||||||
# Search for the first track to get artist name
|
|
||||||
search_results = spotify_client.search_tracks(first_item.get('name', ''), limit=1)
|
|
||||||
if search_results and len(search_results) > 0:
|
|
||||||
artist_name = search_results[0].artists[0] if search_results[0].artists else "Unknown Artist"
|
|
||||||
print(f"Inferred artist name from search: {artist_name}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not infer artist name: {e}")
|
|
||||||
artist_name = "Unknown Artist"
|
|
||||||
|
|
||||||
print(f"Checking completion for artist: {artist_name}")
|
|
||||||
|
|
||||||
# Process albums
|
|
||||||
for album in discography.get('albums', []):
|
|
||||||
completion_data = _check_album_completion(db, album, artist_name, test_mode)
|
|
||||||
albums_completion.append(completion_data)
|
|
||||||
|
|
||||||
# Process singles/EPs
|
|
||||||
for single in discography.get('singles', []):
|
|
||||||
completion_data = _check_single_completion(db, single, artist_name, test_mode)
|
|
||||||
singles_completion.append(completion_data)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"albums": albums_completion,
|
|
||||||
"singles": singles_completion
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error checking discography completion: {e}")
|
print(f"Error checking discography completion: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: bool = False) -> dict:
|
|
||||||
"""Check completion status for a single album"""
|
|
||||||
try:
|
|
||||||
album_name = album_data.get('name', '')
|
|
||||||
total_tracks = album_data.get('total_tracks', 0)
|
|
||||||
album_id = album_data.get('id', '')
|
|
||||||
|
|
||||||
# If total_tracks is 0 (Discogs masters don't include track counts),
|
|
||||||
# try to fetch the real count from the source
|
|
||||||
if total_tracks == 0 and album_id:
|
|
||||||
try:
|
|
||||||
fallback = _get_metadata_fallback_client()
|
|
||||||
album_detail = fallback.get_album_tracks(str(album_id))
|
|
||||||
if album_detail and album_detail.get('items'):
|
|
||||||
total_tracks = len(album_detail['items'])
|
|
||||||
logger.debug(f"Fetched track count for '{album_name}': {total_tracks}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print(f"Checking album: '{album_name}' ({total_tracks} tracks)")
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
if test_mode:
|
|
||||||
# Generate test data to demonstrate the feature
|
|
||||||
import random
|
|
||||||
owned_tracks = random.randint(0, max(1, total_tracks))
|
|
||||||
expected_tracks = total_tracks
|
|
||||||
confidence = random.uniform(0.7, 1.0)
|
|
||||||
db_album = True # Simulate found album
|
|
||||||
print(f"TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'")
|
|
||||||
else:
|
|
||||||
# Check if album exists in database with completeness info
|
|
||||||
try:
|
|
||||||
# Get active server for database checking
|
|
||||||
active_server = config_manager.get_active_media_server()
|
|
||||||
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
|
||||||
title=album_name,
|
|
||||||
artist=artist_name,
|
|
||||||
expected_track_count=total_tracks if total_tracks > 0 else None,
|
|
||||||
confidence_threshold=0.7, # Slightly lower threshold for better matching
|
|
||||||
server_source=active_server # Check only the active server
|
|
||||||
)
|
|
||||||
except Exception as db_error:
|
|
||||||
print(f"Database error for album '{album_name}': {db_error}")
|
|
||||||
# Return error state for this album
|
|
||||||
return {
|
|
||||||
"id": album_id,
|
|
||||||
"name": album_name,
|
|
||||||
"status": "error",
|
|
||||||
"owned_tracks": 0,
|
|
||||||
"expected_tracks": total_tracks,
|
|
||||||
"completion_percentage": 0,
|
|
||||||
"confidence": 0.0,
|
|
||||||
"found_in_db": False,
|
|
||||||
"error_message": str(db_error),
|
|
||||||
"formats": []
|
|
||||||
}
|
|
||||||
|
|
||||||
# Calculate completion percentage
|
|
||||||
if expected_tracks > 0:
|
|
||||||
completion_percentage = (owned_tracks / expected_tracks) * 100
|
|
||||||
elif total_tracks > 0:
|
|
||||||
completion_percentage = (owned_tracks / total_tracks) * 100
|
|
||||||
else:
|
|
||||||
completion_percentage = 100 if owned_tracks > 0 else 0
|
|
||||||
|
|
||||||
# Determine completion status — exact match, no percentage rounding
|
|
||||||
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
|
||||||
status = "completed"
|
|
||||||
elif owned_tracks > 0:
|
|
||||||
status = "partial"
|
|
||||||
else:
|
|
||||||
status = "missing"
|
|
||||||
|
|
||||||
print(f" Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": album_id,
|
|
||||||
"name": album_name,
|
|
||||||
"status": status,
|
|
||||||
"owned_tracks": owned_tracks,
|
|
||||||
"expected_tracks": expected_tracks or total_tracks,
|
|
||||||
"completion_percentage": round(completion_percentage, 1),
|
|
||||||
"confidence": round(confidence, 2) if confidence else 0.0,
|
|
||||||
"found_in_db": db_album is not None,
|
|
||||||
"formats": formats
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
|
|
||||||
return {
|
|
||||||
"id": album_data.get('id', ''),
|
|
||||||
"name": album_data.get('name', 'Unknown'),
|
|
||||||
"status": "error",
|
|
||||||
"owned_tracks": 0,
|
|
||||||
"expected_tracks": album_data.get('total_tracks', 0),
|
|
||||||
"completion_percentage": 0,
|
|
||||||
"confidence": 0.0,
|
|
||||||
"found_in_db": False,
|
|
||||||
"formats": []
|
|
||||||
}
|
|
||||||
|
|
||||||
def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: bool = False) -> dict:
|
|
||||||
"""Check completion status for a single/EP (treat EPs like albums, singles as single tracks)"""
|
|
||||||
try:
|
|
||||||
single_name = single_data.get('name', '')
|
|
||||||
total_tracks = single_data.get('total_tracks', 1)
|
|
||||||
single_id = single_data.get('id', '')
|
|
||||||
album_type = single_data.get('album_type', 'single')
|
|
||||||
formats = []
|
|
||||||
|
|
||||||
print(f"Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
|
|
||||||
|
|
||||||
if test_mode:
|
|
||||||
# Generate test data for singles/EPs
|
|
||||||
import random
|
|
||||||
if album_type == 'ep' or total_tracks > 1:
|
|
||||||
owned_tracks = random.randint(0, total_tracks)
|
|
||||||
expected_tracks = total_tracks
|
|
||||||
confidence = random.uniform(0.7, 1.0)
|
|
||||||
print(f"TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
|
|
||||||
else:
|
|
||||||
owned_tracks = random.choice([0, 1]) # 50/50 chance
|
|
||||||
expected_tracks = 1
|
|
||||||
confidence = random.uniform(0.7, 1.0) if owned_tracks else 0.0
|
|
||||||
print(f"TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks")
|
|
||||||
elif album_type == 'ep' or total_tracks > 1:
|
|
||||||
# Treat EPs like albums
|
|
||||||
try:
|
|
||||||
# Get active server for database checking
|
|
||||||
active_server = config_manager.get_active_media_server()
|
|
||||||
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
|
||||||
title=single_name,
|
|
||||||
artist=artist_name,
|
|
||||||
expected_track_count=total_tracks,
|
|
||||||
confidence_threshold=0.7,
|
|
||||||
server_source=active_server # Check only the active server
|
|
||||||
)
|
|
||||||
except Exception as db_error:
|
|
||||||
print(f"Database error for EP '{single_name}': {db_error}")
|
|
||||||
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
|
|
||||||
|
|
||||||
# Calculate completion percentage
|
|
||||||
if expected_tracks > 0:
|
|
||||||
completion_percentage = (owned_tracks / expected_tracks) * 100
|
|
||||||
else:
|
|
||||||
completion_percentage = (owned_tracks / total_tracks) * 100
|
|
||||||
|
|
||||||
# Determine status — exact match, no percentage rounding
|
|
||||||
if owned_tracks > 0 and owned_tracks >= (expected_tracks or total_tracks):
|
|
||||||
status = "completed"
|
|
||||||
elif owned_tracks > 0:
|
|
||||||
status = "partial"
|
|
||||||
else:
|
|
||||||
status = "missing"
|
|
||||||
|
|
||||||
print(f" EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Single track - just check if the track exists
|
|
||||||
try:
|
|
||||||
active_server = config_manager.get_active_media_server()
|
|
||||||
db_track, confidence = db.check_track_exists(
|
|
||||||
title=single_name,
|
|
||||||
artist=artist_name,
|
|
||||||
confidence_threshold=0.7,
|
|
||||||
server_source=active_server
|
|
||||||
)
|
|
||||||
except Exception as db_error:
|
|
||||||
print(f"Database error for single '{single_name}': {db_error}")
|
|
||||||
db_track, confidence = None, 0.0
|
|
||||||
|
|
||||||
owned_tracks = 1 if db_track else 0
|
|
||||||
expected_tracks = 1
|
|
||||||
completion_percentage = 100 if db_track else 0
|
|
||||||
|
|
||||||
status = "completed" if db_track else "missing"
|
|
||||||
|
|
||||||
# Extract format from single track
|
|
||||||
if db_track and db_track.file_path:
|
|
||||||
import os
|
|
||||||
ext = os.path.splitext(db_track.file_path)[1].lstrip('.').upper()
|
|
||||||
if ext == 'MP3' and db_track.bitrate:
|
|
||||||
formats = [f"MP3-{db_track.bitrate}"]
|
|
||||||
elif ext:
|
|
||||||
formats = [ext]
|
|
||||||
|
|
||||||
print(f" Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": single_id,
|
|
||||||
"name": single_name,
|
|
||||||
"status": status,
|
|
||||||
"owned_tracks": owned_tracks,
|
|
||||||
"expected_tracks": expected_tracks or total_tracks,
|
|
||||||
"completion_percentage": round(completion_percentage, 1),
|
|
||||||
"confidence": round(confidence, 2) if confidence else 0.0,
|
|
||||||
"found_in_db": (db_album if album_type == 'ep' or total_tracks > 1 else db_track) is not None,
|
|
||||||
"type": album_type,
|
|
||||||
"formats": formats
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
|
|
||||||
return {
|
|
||||||
"id": single_data.get('id', ''),
|
|
||||||
"name": single_data.get('name', 'Unknown'),
|
|
||||||
"status": "error",
|
|
||||||
"owned_tracks": 0,
|
|
||||||
"expected_tracks": single_data.get('total_tracks', 1),
|
|
||||||
"completion_percentage": 0,
|
|
||||||
"confidence": 0.0,
|
|
||||||
"found_in_db": False,
|
|
||||||
"type": single_data.get('album_type', 'single'),
|
|
||||||
"formats": []
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.route('/api/artist/<artist_id>/completion-stream', methods=['POST'])
|
@app.route('/api/artist/<artist_id>/completion-stream', methods=['POST'])
|
||||||
def check_artist_discography_completion_stream(artist_id):
|
def check_artist_discography_completion_stream(artist_id):
|
||||||
"""Stream completion status for artist's albums and singles one by one"""
|
"""Stream completion status for artist's albums and singles one by one"""
|
||||||
|
|
@ -11984,75 +11730,22 @@ def check_artist_discography_completion_stream(artist_id):
|
||||||
|
|
||||||
# Extract data for the generator
|
# Extract data for the generator
|
||||||
discography = data['discography']
|
discography = data['discography']
|
||||||
test_mode = data.get('test_mode', False)
|
|
||||||
artist_name = data.get('artist_name', 'Unknown Artist')
|
artist_name = data.get('artist_name', 'Unknown Artist')
|
||||||
|
source_override = (data.get('source') or '').strip().lower() or None
|
||||||
|
from core.metadata_service import iter_artist_discography_completion_events
|
||||||
|
|
||||||
def generate_completion_stream():
|
def generate_completion_stream():
|
||||||
try:
|
try:
|
||||||
print(f"Starting streaming completion check for artist: {artist_name}")
|
print(f"Starting streaming completion check for artist: {artist_name}")
|
||||||
|
for event in iter_artist_discography_completion_events(
|
||||||
# Get database instance
|
discography,
|
||||||
from database.music_database import MusicDatabase
|
artist_name=artist_name,
|
||||||
db = MusicDatabase()
|
source_override=source_override,
|
||||||
|
):
|
||||||
# Process albums one by one
|
yield f"data: {json.dumps(event)}\n\n"
|
||||||
total_items = len(discography.get('albums', [])) + len(discography.get('singles', []))
|
if event.get('type') in ('album_completion', 'single_completion'):
|
||||||
processed_count = 0
|
|
||||||
|
|
||||||
# Send initial status
|
|
||||||
yield f"data: {json.dumps({'type': 'start', 'total_items': total_items, 'artist_name': artist_name})}\n\n"
|
|
||||||
|
|
||||||
# Process albums
|
|
||||||
for album in discography.get('albums', []):
|
|
||||||
try:
|
|
||||||
completion_data = _check_album_completion(db, album, artist_name, test_mode)
|
|
||||||
completion_data['type'] = 'album_completion'
|
|
||||||
completion_data['container_type'] = 'albums'
|
|
||||||
processed_count += 1
|
|
||||||
completion_data['progress'] = round((processed_count / total_items) * 100, 1)
|
|
||||||
|
|
||||||
yield f"data: {json.dumps(completion_data)}\n\n"
|
|
||||||
|
|
||||||
# Small delay to make the streaming effect visible
|
# Small delay to make the streaming effect visible
|
||||||
time.sleep(0.1) # 100ms delay between items
|
time.sleep(0.1) # 100ms delay between items
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_data = {
|
|
||||||
'type': 'error',
|
|
||||||
'container_type': 'albums',
|
|
||||||
'id': album.get('id', ''),
|
|
||||||
'name': album.get('name', 'Unknown'),
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(error_data)}\n\n"
|
|
||||||
|
|
||||||
# Process singles/EPs
|
|
||||||
for single in discography.get('singles', []):
|
|
||||||
try:
|
|
||||||
completion_data = _check_single_completion(db, single, artist_name, test_mode)
|
|
||||||
completion_data['type'] = 'single_completion'
|
|
||||||
completion_data['container_type'] = 'singles'
|
|
||||||
processed_count += 1
|
|
||||||
completion_data['progress'] = round((processed_count / total_items) * 100, 1)
|
|
||||||
|
|
||||||
yield f"data: {json.dumps(completion_data)}\n\n"
|
|
||||||
|
|
||||||
# Small delay to make the streaming effect visible
|
|
||||||
time.sleep(0.1) # 100ms delay between items
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_data = {
|
|
||||||
'type': 'error',
|
|
||||||
'container_type': 'singles',
|
|
||||||
'id': single.get('id', ''),
|
|
||||||
'name': single.get('name', 'Unknown'),
|
|
||||||
'error': str(e)
|
|
||||||
}
|
|
||||||
yield f"data: {json.dumps(error_data)}\n\n"
|
|
||||||
|
|
||||||
# Send completion signal
|
|
||||||
yield f"data: {json.dumps({'type': 'complete', 'processed_count': processed_count})}\n\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in streaming completion check: {e}")
|
print(f"Error in streaming completion check: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -12084,8 +11777,8 @@ def library_completion_stream():
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
try:
|
try:
|
||||||
from database.music_database import MusicDatabase
|
from core.metadata_service import check_album_completion, check_single_completion
|
||||||
db = MusicDatabase()
|
db = get_database()
|
||||||
|
|
||||||
categories = ['albums', 'eps', 'singles']
|
categories = ['albums', 'eps', 'singles']
|
||||||
all_items = []
|
all_items = []
|
||||||
|
|
@ -12106,9 +11799,9 @@ def library_completion_stream():
|
||||||
}
|
}
|
||||||
|
|
||||||
if category == 'singles':
|
if category == 'singles':
|
||||||
result = _check_single_completion(db, mapped, artist_name)
|
result = check_single_completion(db, mapped, artist_name)
|
||||||
else:
|
else:
|
||||||
result = _check_album_completion(db, mapped, artist_name)
|
result = check_album_completion(db, mapped, artist_name)
|
||||||
|
|
||||||
result['spotify_id'] = item.get('spotify_id', '')
|
result['spotify_id'] = item.get('spotify_id', '')
|
||||||
result['category'] = category
|
result['category'] = category
|
||||||
|
|
|
||||||
|
|
@ -35807,7 +35807,8 @@ async function loadArtistDiscography(artistId, artistName = null, sourceOverride
|
||||||
|
|
||||||
const discography = {
|
const discography = {
|
||||||
albums: data.albums || [],
|
albums: data.albums || [],
|
||||||
singles: data.singles || []
|
singles: data.singles || [],
|
||||||
|
source: data.source || sourceOverride || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update selected artist with full details from backend (includes MusicBrainz ID)
|
// Update selected artist with full details from backend (includes MusicBrainz ID)
|
||||||
|
|
@ -36303,7 +36304,7 @@ async function checkDiscographyCompletion(artistId, discography) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
discography: discography,
|
discography: discography,
|
||||||
artist_name: artistsPageState.selectedArtist?.name || 'Unknown Artist',
|
artist_name: artistsPageState.selectedArtist?.name || 'Unknown Artist',
|
||||||
test_mode: window.location.search.includes('test=true')
|
source: discography?.source || artistsPageState.sourceOverride || null,
|
||||||
}),
|
}),
|
||||||
signal: artistCompletionController.signal
|
signal: artistCompletionController.signal
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue