Add format summary tags to library release cards, wishlist modal, and artist hero
This commit is contained in:
parent
3663a75769
commit
6c6651b879
5 changed files with 211 additions and 44 deletions
|
|
@ -2163,12 +2163,13 @@ class MusicDatabase:
|
||||||
|
|
||||||
return max(0.0, similarity)
|
return max(0.0, similarity)
|
||||||
|
|
||||||
def check_album_completeness(self, album_id: int, expected_track_count: Optional[int] = None) -> Tuple[int, int, bool]:
|
def check_album_completeness(self, album_id: int, expected_track_count: Optional[int] = None) -> Tuple[int, int, bool, List[str]]:
|
||||||
"""
|
"""
|
||||||
Check if we have all tracks for an album.
|
Check if we have all tracks for an album.
|
||||||
Merges counts across split album entries (same title+year+artist) so that
|
Merges counts across split album entries (same title+year+artist) so that
|
||||||
albums split by the media server (e.g. Navidrome) are treated as one.
|
albums split by the media server (e.g. Navidrome) are treated as one.
|
||||||
Returns (owned_tracks, expected_tracks, is_complete)
|
Returns (owned_tracks, expected_tracks, is_complete, formats)
|
||||||
|
where formats is a list of distinct format strings like ["FLAC"] or ["FLAC", "MP3-320"]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
|
|
@ -2179,7 +2180,7 @@ class MusicDatabase:
|
||||||
album_info = cursor.fetchone()
|
album_info = cursor.fetchone()
|
||||||
|
|
||||||
if not album_info:
|
if not album_info:
|
||||||
return 0, 0, False
|
return 0, 0, False, []
|
||||||
|
|
||||||
# Find all album IDs that share the same title, year, and artist
|
# Find all album IDs that share the same title, year, and artist
|
||||||
# This merges split albums (e.g. Navidrome splitting one album into multiple entries)
|
# This merges split albums (e.g. Navidrome splitting one album into multiple entries)
|
||||||
|
|
@ -2201,7 +2202,7 @@ class MusicDatabase:
|
||||||
|
|
||||||
# Use provided expected count if available, otherwise use stored count
|
# Use provided expected count if available, otherwise use stored count
|
||||||
expected_tracks = expected_track_count if expected_track_count is not None else stored_track_count
|
expected_tracks = expected_track_count if expected_track_count is not None else stored_track_count
|
||||||
|
|
||||||
# Determine completeness with refined thresholds
|
# Determine completeness with refined thresholds
|
||||||
if expected_tracks and expected_tracks > 0:
|
if expected_tracks and expected_tracks > 0:
|
||||||
completion_ratio = owned_tracks / expected_tracks
|
completion_ratio = owned_tracks / expected_tracks
|
||||||
|
|
@ -2210,34 +2211,64 @@ class MusicDatabase:
|
||||||
else:
|
else:
|
||||||
# Fallback: if we have any tracks, consider it owned
|
# Fallback: if we have any tracks, consider it owned
|
||||||
is_complete = owned_tracks > 0
|
is_complete = owned_tracks > 0
|
||||||
|
|
||||||
return owned_tracks, expected_tracks or 0, is_complete
|
# Get distinct format strings for owned tracks
|
||||||
|
formats = self._get_album_formats(cursor, sibling_ids)
|
||||||
|
|
||||||
|
return owned_tracks, expected_tracks or 0, is_complete, formats
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking album completeness for album_id {album_id}: {e}")
|
logger.error(f"Error checking album completeness for album_id {album_id}: {e}")
|
||||||
return 0, 0, False
|
return 0, 0, False, []
|
||||||
|
|
||||||
|
def _get_album_formats(self, cursor, sibling_ids: list) -> List[str]:
|
||||||
|
"""Get distinct format strings for tracks in the given album IDs."""
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
placeholders = ','.join('?' for _ in sibling_ids)
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT DISTINCT file_path, bitrate FROM tracks
|
||||||
|
WHERE album_id IN ({placeholders}) AND file_path IS NOT NULL
|
||||||
|
""", sibling_ids)
|
||||||
|
|
||||||
|
format_set = set()
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
ext = os.path.splitext(row['file_path'] or '')[1].lstrip('.').upper()
|
||||||
|
if not ext:
|
||||||
|
continue
|
||||||
|
if ext == 'MP3' and row['bitrate']:
|
||||||
|
format_set.add(f"MP3-{row['bitrate']}")
|
||||||
|
elif ext == 'MP3':
|
||||||
|
format_set.add('MP3')
|
||||||
|
else:
|
||||||
|
# FLAC, OGG, AAC, etc. — just the extension
|
||||||
|
format_set.add(ext)
|
||||||
|
return sorted(format_set)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting album formats: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def check_album_exists_with_completeness(self, title: str, artist: str, expected_track_count: Optional[int] = None, confidence_threshold: float = 0.8, server_source: Optional[str] = None) -> Tuple[Optional[DatabaseAlbum], float, int, int, bool]:
|
def check_album_exists_with_completeness(self, title: str, artist: str, expected_track_count: Optional[int] = None, confidence_threshold: float = 0.8, server_source: Optional[str] = None) -> Tuple[Optional[DatabaseAlbum], float, int, int, bool, List[str]]:
|
||||||
"""
|
"""
|
||||||
Check if an album exists in the database with completeness information.
|
Check if an album exists in the database with completeness information.
|
||||||
Enhanced to handle edition matching (standard <-> deluxe variants).
|
Enhanced to handle edition matching (standard <-> deluxe variants).
|
||||||
Returns (album, confidence, owned_tracks, expected_tracks, is_complete)
|
Returns (album, confidence, owned_tracks, expected_tracks, is_complete, formats)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Try enhanced edition-aware matching first with expected track count for Smart Edition Matching
|
# Try enhanced edition-aware matching first with expected track count for Smart Edition Matching
|
||||||
album, confidence = self.check_album_exists_with_editions(title, artist, confidence_threshold, expected_track_count, server_source)
|
album, confidence = self.check_album_exists_with_editions(title, artist, confidence_threshold, expected_track_count, server_source)
|
||||||
|
|
||||||
if not album:
|
if not album:
|
||||||
return None, 0.0, 0, 0, False
|
return None, 0.0, 0, 0, False, []
|
||||||
|
|
||||||
# Now check completeness
|
# Now check completeness (includes formats)
|
||||||
owned_tracks, expected_tracks, is_complete = self.check_album_completeness(album.id, expected_track_count)
|
owned_tracks, expected_tracks, is_complete, formats = self.check_album_completeness(album.id, expected_track_count)
|
||||||
|
|
||||||
return album, confidence, owned_tracks, expected_tracks, is_complete
|
return album, confidence, owned_tracks, expected_tracks, is_complete, formats
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}")
|
logger.error(f"Error checking album existence with completeness for '{title}' by '{artist}': {e}")
|
||||||
return None, 0.0, 0, 0, False
|
return None, 0.0, 0, 0, False, []
|
||||||
|
|
||||||
def check_album_exists_with_editions(self, title: str, artist: str, confidence_threshold: float = 0.8, expected_track_count: Optional[int] = None, server_source: Optional[str] = None) -> Tuple[Optional[DatabaseAlbum], float]:
|
def check_album_exists_with_editions(self, title: str, artist: str, confidence_threshold: float = 0.8, expected_track_count: Optional[int] = None, server_source: Optional[str] = None) -> Tuple[Optional[DatabaseAlbum], float]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -769,7 +769,7 @@ class DatabaseLibraryWorker(QThread):
|
||||||
|
|
||||||
# Search database for this combination with completeness info
|
# Search database for this combination with completeness info
|
||||||
print(f" 🔍 Searching database: album='{album_name}', artist='{artist_clean}'")
|
print(f" 🔍 Searching database: album='{album_name}', artist='{artist_clean}'")
|
||||||
db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness(
|
db_album, confidence, owned_tracks, expected_tracks, is_complete, *_ = db.check_album_exists_with_completeness(
|
||||||
album_name, artist_clean, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
album_name, artist_clean, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -788,7 +788,7 @@ class DatabaseLibraryWorker(QThread):
|
||||||
# Backup search with original uncleaned artist name
|
# Backup search with original uncleaned artist name
|
||||||
if not db_album and artist and artist != artist_clean:
|
if not db_album and artist and artist != artist_clean:
|
||||||
print(f" 🔄 Backup search with original artist: album='{album_name}', artist='{artist}'")
|
print(f" 🔄 Backup search with original artist: album='{album_name}', artist='{artist}'")
|
||||||
db_album_backup, confidence_backup, owned_backup, expected_backup, complete_backup = db.check_album_exists_with_completeness(
|
db_album_backup, confidence_backup, owned_backup, expected_backup, complete_backup, *_ = db.check_album_exists_with_completeness(
|
||||||
album_name, artist, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
album_name, artist, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -805,7 +805,7 @@ class DatabaseLibraryWorker(QThread):
|
||||||
artist_no_comma = artist.replace(',', '').strip()
|
artist_no_comma = artist.replace(',', '').strip()
|
||||||
artist_no_comma = ' '.join(artist_no_comma.split())
|
artist_no_comma = ' '.join(artist_no_comma.split())
|
||||||
print(f" 🔄 Comma-removal fallback: album='{album_name}', artist='{artist_no_comma}'")
|
print(f" 🔄 Comma-removal fallback: album='{album_name}', artist='{artist_no_comma}'")
|
||||||
db_album_comma, confidence_comma, owned_comma, expected_comma, complete_comma = db.check_album_exists_with_completeness(
|
db_album_comma, confidence_comma, owned_comma, expected_comma, complete_comma, *_ = db.check_album_exists_with_completeness(
|
||||||
album_name, artist_no_comma, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
album_name, artist_no_comma, expected_track_count, confidence_threshold=0.7, server_source=active_server
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5727,6 +5727,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
|
||||||
|
|
||||||
print(f"🔍 Checking album: '{album_name}' ({total_tracks} tracks)")
|
print(f"🔍 Checking album: '{album_name}' ({total_tracks} tracks)")
|
||||||
|
|
||||||
|
formats = []
|
||||||
if test_mode:
|
if test_mode:
|
||||||
# Generate test data to demonstrate the feature
|
# Generate test data to demonstrate the feature
|
||||||
import random
|
import random
|
||||||
|
|
@ -5740,7 +5741,7 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
|
||||||
try:
|
try:
|
||||||
# Get active server for database checking
|
# Get active server for database checking
|
||||||
active_server = config_manager.get_active_media_server()
|
active_server = config_manager.get_active_media_server()
|
||||||
db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness(
|
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
||||||
title=album_name,
|
title=album_name,
|
||||||
artist=artist_name,
|
artist=artist_name,
|
||||||
expected_track_count=total_tracks if total_tracks > 0 else None,
|
expected_track_count=total_tracks if total_tracks > 0 else None,
|
||||||
|
|
@ -5790,9 +5791,10 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
|
||||||
"expected_tracks": expected_tracks or total_tracks,
|
"expected_tracks": expected_tracks or total_tracks,
|
||||||
"completion_percentage": round(completion_percentage, 1),
|
"completion_percentage": round(completion_percentage, 1),
|
||||||
"confidence": round(confidence, 2) if confidence else 0.0,
|
"confidence": round(confidence, 2) if confidence else 0.0,
|
||||||
"found_in_db": db_album is not None
|
"found_in_db": db_album is not None,
|
||||||
|
"formats": formats
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
|
print(f"❌ Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
|
||||||
return {
|
return {
|
||||||
|
|
@ -5803,7 +5805,8 @@ def _check_album_completion(db, album_data: dict, artist_name: str, test_mode: b
|
||||||
"expected_tracks": album_data.get('total_tracks', 0),
|
"expected_tracks": album_data.get('total_tracks', 0),
|
||||||
"completion_percentage": 0,
|
"completion_percentage": 0,
|
||||||
"confidence": 0.0,
|
"confidence": 0.0,
|
||||||
"found_in_db": False
|
"found_in_db": False,
|
||||||
|
"formats": []
|
||||||
}
|
}
|
||||||
|
|
||||||
def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: bool = False) -> dict:
|
def _check_single_completion(db, single_data: dict, artist_name: str, test_mode: bool = False) -> dict:
|
||||||
|
|
@ -5813,14 +5816,15 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
total_tracks = single_data.get('total_tracks', 1)
|
total_tracks = single_data.get('total_tracks', 1)
|
||||||
single_id = single_data.get('id', '')
|
single_id = single_data.get('id', '')
|
||||||
album_type = single_data.get('album_type', 'single')
|
album_type = single_data.get('album_type', 'single')
|
||||||
|
formats = []
|
||||||
|
|
||||||
print(f"🎵 Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
|
print(f"🎵 Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
|
||||||
|
|
||||||
if test_mode:
|
if test_mode:
|
||||||
# Generate test data for singles/EPs
|
# Generate test data for singles/EPs
|
||||||
import random
|
import random
|
||||||
if album_type == 'ep' or total_tracks > 1:
|
if album_type == 'ep' or total_tracks > 1:
|
||||||
owned_tracks = random.randint(0, total_tracks)
|
owned_tracks = random.randint(0, total_tracks)
|
||||||
expected_tracks = total_tracks
|
expected_tracks = total_tracks
|
||||||
confidence = random.uniform(0.7, 1.0)
|
confidence = random.uniform(0.7, 1.0)
|
||||||
print(f"🧪 TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
|
print(f"🧪 TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
|
||||||
|
|
@ -5834,7 +5838,7 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
try:
|
try:
|
||||||
# Get active server for database checking
|
# Get active server for database checking
|
||||||
active_server = config_manager.get_active_media_server()
|
active_server = config_manager.get_active_media_server()
|
||||||
db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness(
|
db_album, confidence, owned_tracks, expected_tracks, is_complete, formats = db.check_album_exists_with_completeness(
|
||||||
title=single_name,
|
title=single_name,
|
||||||
artist=artist_name,
|
artist=artist_name,
|
||||||
expected_track_count=total_tracks,
|
expected_track_count=total_tracks,
|
||||||
|
|
@ -5844,25 +5848,25 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
print(f"⚠️ Database error for EP '{single_name}': {db_error}")
|
print(f"⚠️ Database error for EP '{single_name}': {db_error}")
|
||||||
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
|
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
|
||||||
|
|
||||||
# Calculate completion percentage
|
# Calculate completion percentage
|
||||||
if expected_tracks > 0:
|
if expected_tracks > 0:
|
||||||
completion_percentage = (owned_tracks / expected_tracks) * 100
|
completion_percentage = (owned_tracks / expected_tracks) * 100
|
||||||
else:
|
else:
|
||||||
completion_percentage = (owned_tracks / total_tracks) * 100
|
completion_percentage = (owned_tracks / total_tracks) * 100
|
||||||
|
|
||||||
# Determine status
|
# Determine status
|
||||||
if completion_percentage >= 90 and owned_tracks > 0:
|
if completion_percentage >= 90 and owned_tracks > 0:
|
||||||
status = "completed"
|
status = "completed"
|
||||||
elif completion_percentage >= 60:
|
elif completion_percentage >= 60:
|
||||||
status = "nearly_complete"
|
status = "nearly_complete"
|
||||||
elif completion_percentage > 0:
|
elif completion_percentage > 0:
|
||||||
status = "partial"
|
status = "partial"
|
||||||
else:
|
else:
|
||||||
status = "missing"
|
status = "missing"
|
||||||
|
|
||||||
print(f" 📊 EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
print(f" 📊 EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Single track - just check if the track exists
|
# Single track - just check if the track exists
|
||||||
try:
|
try:
|
||||||
|
|
@ -5874,15 +5878,24 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
print(f"⚠️ Database error for single '{single_name}': {db_error}")
|
print(f"⚠️ Database error for single '{single_name}': {db_error}")
|
||||||
db_track, confidence = None, 0.0
|
db_track, confidence = None, 0.0
|
||||||
|
|
||||||
owned_tracks = 1 if db_track else 0
|
owned_tracks = 1 if db_track else 0
|
||||||
expected_tracks = 1
|
expected_tracks = 1
|
||||||
completion_percentage = 100 if db_track else 0
|
completion_percentage = 100 if db_track else 0
|
||||||
|
|
||||||
status = "completed" if db_track else "missing"
|
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}")
|
print(f" 🎵 Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": single_id,
|
"id": single_id,
|
||||||
"name": single_name,
|
"name": single_name,
|
||||||
|
|
@ -5892,9 +5905,10 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
"completion_percentage": round(completion_percentage, 1),
|
"completion_percentage": round(completion_percentage, 1),
|
||||||
"confidence": round(confidence, 2) if confidence else 0.0,
|
"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,
|
"found_in_db": (db_album if album_type == 'ep' or total_tracks > 1 else db_track) is not None,
|
||||||
"type": album_type
|
"type": album_type,
|
||||||
|
"formats": formats
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
|
print(f"❌ Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
|
||||||
return {
|
return {
|
||||||
|
|
@ -5906,7 +5920,8 @@ def _check_single_completion(db, single_data: dict, artist_name: str, test_mode:
|
||||||
"completion_percentage": 0,
|
"completion_percentage": 0,
|
||||||
"confidence": 0.0,
|
"confidence": 0.0,
|
||||||
"found_in_db": False,
|
"found_in_db": False,
|
||||||
"type": single_data.get('album_type', 'single')
|
"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'])
|
||||||
|
|
|
||||||
|
|
@ -11126,6 +11126,31 @@ async function lazyLoadTrackOwnership(artistName, tracks, sourceCard) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregate format summary from owned tracks
|
||||||
|
const formatSet = new Set();
|
||||||
|
for (const trackName of Object.keys(ownership)) {
|
||||||
|
const td = ownership[trackName];
|
||||||
|
if (td && td.owned && td.format) {
|
||||||
|
if (td.format === 'MP3' && td.bitrate) {
|
||||||
|
formatSet.add(`MP3-${td.bitrate}`);
|
||||||
|
} else {
|
||||||
|
formatSet.add(td.format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formatSet.size > 0) {
|
||||||
|
const heroDetailsContainer = document.querySelector('.add-to-wishlist-modal-hero-details');
|
||||||
|
if (heroDetailsContainer) {
|
||||||
|
// Remove any existing format tag
|
||||||
|
const existing = heroDetailsContainer.querySelector('.modal-format-tag');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
const formatTag = document.createElement('span');
|
||||||
|
formatTag.className = 'modal-format-tag';
|
||||||
|
formatTag.textContent = [...formatSet].sort().join(' / ');
|
||||||
|
heroDetailsContainer.appendChild(formatTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update hero subtitle with missing count
|
// Update hero subtitle with missing count
|
||||||
const missingCount = tracks.length - ownedCount;
|
const missingCount = tracks.length - ownedCount;
|
||||||
const heroDetails = document.querySelectorAll('.add-to-wishlist-modal-hero-detail');
|
const heroDetails = document.querySelectorAll('.add-to-wishlist-modal-hero-detail');
|
||||||
|
|
@ -26092,6 +26117,10 @@ function updateArtistGenres(genres) {
|
||||||
|
|
||||||
genresContainer.innerHTML = "";
|
genresContainer.innerHTML = "";
|
||||||
|
|
||||||
|
// Clear any previous artist format tags (they arrive later via streaming)
|
||||||
|
const oldFormats = genresContainer.parentElement?.querySelector('.artist-formats');
|
||||||
|
if (oldFormats) oldFormats.remove();
|
||||||
|
|
||||||
if (genres && genres.length > 0) {
|
if (genres && genres.length > 0) {
|
||||||
genres.forEach(genre => {
|
genres.forEach(genre => {
|
||||||
const genreTag = document.createElement("span");
|
const genreTag = document.createElement("span");
|
||||||
|
|
@ -26586,6 +26615,7 @@ async function checkLibraryCompletion(artistName, discography) {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
let ownedCounts = { albums: 0, eps: 0, singles: 0 };
|
let ownedCounts = { albums: 0, eps: 0, singles: 0 };
|
||||||
let totalCounts = { albums: 0, eps: 0, singles: 0 };
|
let totalCounts = { albums: 0, eps: 0, singles: 0 };
|
||||||
|
const artistFormatSet = new Set();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
|
|
@ -26604,6 +26634,10 @@ async function checkLibraryCompletion(artistName, discography) {
|
||||||
totalCounts[eventData.category]++;
|
totalCounts[eventData.category]++;
|
||||||
if (eventData.status !== 'missing' && eventData.status !== 'error') {
|
if (eventData.status !== 'missing' && eventData.status !== 'error') {
|
||||||
ownedCounts[eventData.category]++;
|
ownedCounts[eventData.category]++;
|
||||||
|
// Accumulate formats for artist-level summary
|
||||||
|
if (eventData.formats) {
|
||||||
|
eventData.formats.forEach(f => artistFormatSet.add(f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update stats incrementally
|
// Update stats incrementally
|
||||||
updateCategoryStatsFromStream(
|
updateCategoryStatsFromStream(
|
||||||
|
|
@ -26614,7 +26648,7 @@ async function checkLibraryCompletion(artistName, discography) {
|
||||||
} else if (eventData.type === 'complete') {
|
} else if (eventData.type === 'complete') {
|
||||||
console.log(`✅ Library completion stream done: ${eventData.processed_count} items`);
|
console.log(`✅ Library completion stream done: ${eventData.processed_count} items`);
|
||||||
// Final stats recalculation
|
// Final stats recalculation
|
||||||
recalculateSummaryStats();
|
recalculateSummaryStats(artistFormatSet);
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.warn('Error parsing SSE event:', parseError, line);
|
console.warn('Error parsing SSE event:', parseError, line);
|
||||||
|
|
@ -26701,6 +26735,22 @@ function updateLibraryReleaseCard(data) {
|
||||||
completionFill.classList.add('missing');
|
completionFill.classList.add('missing');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display format tags on owned releases
|
||||||
|
if (isOwned && data.formats && data.formats.length > 0) {
|
||||||
|
// Store formats on release data for modal use
|
||||||
|
if (card._releaseData) {
|
||||||
|
card._releaseData.formats = data.formats;
|
||||||
|
}
|
||||||
|
// Remove any existing format tags
|
||||||
|
const existingFormats = card.querySelector('.release-formats');
|
||||||
|
if (existingFormats) existingFormats.remove();
|
||||||
|
|
||||||
|
const formatsDiv = document.createElement('div');
|
||||||
|
formatsDiv.className = 'release-formats';
|
||||||
|
formatsDiv.innerHTML = data.formats.map(f => `<span class="release-format-tag">${f}</span>`).join('');
|
||||||
|
card.appendChild(formatsDiv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCategoryStatsFromStream(category, ownedCount, missingCount) {
|
function updateCategoryStatsFromStream(category, ownedCount, missingCount) {
|
||||||
|
|
@ -26734,7 +26784,7 @@ function updateCategoryStatsFromStream(category, ownedCount, missingCount) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalculateSummaryStats() {
|
function recalculateSummaryStats(artistFormatSet) {
|
||||||
const disc = artistDetailPageState.currentDiscography;
|
const disc = artistDetailPageState.currentDiscography;
|
||||||
if (!disc) return;
|
if (!disc) return;
|
||||||
|
|
||||||
|
|
@ -26773,6 +26823,29 @@ function recalculateSummaryStats() {
|
||||||
const completionEl = document.getElementById("completion-percentage");
|
const completionEl = document.getElementById("completion-percentage");
|
||||||
if (completionEl) completionEl.textContent = `${pct}%`;
|
if (completionEl) completionEl.textContent = `${pct}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display artist-level format summary
|
||||||
|
if (artistFormatSet && artistFormatSet.size > 0) {
|
||||||
|
const heroInfo = document.querySelector('.artist-hero-section .artist-info');
|
||||||
|
if (heroInfo) {
|
||||||
|
// Remove any existing artist format tag
|
||||||
|
const existing = heroInfo.querySelector('.artist-formats');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const formatsDiv = document.createElement('div');
|
||||||
|
formatsDiv.className = 'artist-formats';
|
||||||
|
formatsDiv.innerHTML = [...artistFormatSet].sort()
|
||||||
|
.map(f => `<span class="artist-format-tag">${f}</span>`)
|
||||||
|
.join('');
|
||||||
|
// Insert after genres container
|
||||||
|
const genresContainer = heroInfo.querySelector('.artist-genres-container');
|
||||||
|
if (genresContainer && genresContainer.nextSibling) {
|
||||||
|
heroInfo.insertBefore(formatsDiv, genresContainer.nextSibling);
|
||||||
|
} else {
|
||||||
|
heroInfo.appendChild(formatsDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI state management functions
|
// UI state management functions
|
||||||
|
|
|
||||||
|
|
@ -14226,6 +14226,54 @@ body {
|
||||||
color: rgba(255, 255, 255, 0.45);
|
color: rgba(255, 255, 255, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Format tags on release cards ── */
|
||||||
|
.release-formats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 8px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-format-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
color: rgba(29, 185, 84, 0.9);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Format tag in wishlist modal hero ── */
|
||||||
|
.modal-format-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
color: rgba(29, 185, 84, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Format tags in artist hero section ── */
|
||||||
|
.artist-formats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-format-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
color: rgba(29, 185, 84, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal Footer */
|
/* Modal Footer */
|
||||||
.add-to-wishlist-modal-footer {
|
.add-to-wishlist-modal-footer {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue