Add metadata updater system for artists

Introduces a metadata updater backend in web_server.py, including API endpoints for starting, stopping, and checking status, and a threaded worker for updating artist metadata from Spotify. Updates script.js to add UI controls, polling, and progress display for the metadata updater, with logic to hide the tool when Jellyfin is the active server.
This commit is contained in:
Broque Thomas 2025-09-10 19:19:29 -07:00
parent 1dd4f8d7a3
commit ac7a8f1513
2 changed files with 877 additions and 0 deletions

View file

@ -10530,6 +10530,634 @@ def start_watchlist_auto_scanning():
thread.start()
print("✅ Automatic daily watchlist scanning started")
# --- Metadata Updater System ---
from concurrent.futures import ThreadPoolExecutor, as_completed
# Global state for metadata update process
metadata_update_state = {
'status': 'idle',
'current_artist': '',
'processed': 0,
'total': 0,
'percentage': 0.0,
'successful': 0,
'failed': 0,
'started_at': None,
'completed_at': None,
'error': None,
'refresh_interval_days': 30
}
metadata_update_worker = None
metadata_update_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="metadata_update")
@app.route('/api/metadata/start', methods=['POST'])
def start_metadata_update():
"""Start the metadata update process - EXACT copy of dashboard.py logic"""
global metadata_update_worker, metadata_update_state
try:
# Check if already running
if metadata_update_state['status'] == 'running':
return jsonify({"success": False, "error": "Metadata update already running"}), 400
# Get refresh interval from request
data = request.get_json() or {}
refresh_interval_days = data.get('refresh_interval_days', 30)
# Check active server and client availability - EXACTLY like dashboard.py
active_server = config_manager.get_active_media_server()
# Get appropriate media client - EXACTLY like dashboard.py start_metadata_update()
if active_server == "jellyfin":
media_client = jellyfin_client
if not media_client:
add_activity_item("", "Metadata Update", "Jellyfin client not available", "Now")
return jsonify({"success": False, "error": "Jellyfin client not available"}), 400
else: # plex
media_client = plex_client
if not media_client:
add_activity_item("", "Metadata Update", "Plex client not available", "Now")
return jsonify({"success": False, "error": "Plex client not available"}), 400
# DEBUG: Check Plex connection details
print(f"[DEBUG] Active server: {active_server}")
print(f"[DEBUG] Plex client: {media_client}")
if hasattr(media_client, 'server') and media_client.server:
print(f"[DEBUG] Plex server URL: {getattr(media_client.server, '_baseurl', 'NO_URL')}")
print(f"[DEBUG] Plex server name: {getattr(media_client.server, 'friendlyName', 'NO_NAME')}")
# Check available libraries
try:
sections = media_client.server.library.sections()
print(f"[DEBUG] Available Plex libraries: {[(s.title, s.type) for s in sections]}")
except Exception as e:
print(f"[DEBUG] Error getting Plex libraries: {e}")
else:
print(f"[DEBUG] Plex server is NOT connected!")
# Check Spotify client - EXACTLY like dashboard.py
if not spotify_client:
add_activity_item("", "Metadata Update", "Spotify client not available", "Now")
return jsonify({"success": False, "error": "Spotify client not available"}), 400
# Reset state
metadata_update_state.update({
'status': 'running',
'current_artist': 'Loading artists...',
'processed': 0,
'total': 0,
'percentage': 0.0,
'successful': 0,
'failed': 0,
'started_at': datetime.now(),
'completed_at': None,
'error': None,
'refresh_interval_days': refresh_interval_days
})
# Start the metadata update worker - EXACTLY like dashboard.py
def run_metadata_update():
try:
metadata_worker = WebMetadataUpdateWorker(
None, # Artists will be loaded in the worker thread - EXACTLY like dashboard.py
media_client,
spotify_client,
active_server,
refresh_interval_days
)
metadata_worker.run()
except Exception as e:
print(f"Error in metadata update worker: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
add_activity_item("", "Metadata Error", str(e), "Now")
metadata_update_worker = metadata_update_executor.submit(run_metadata_update)
add_activity_item("🎵", "Metadata Update", "Loading artists from library...", "Now")
return jsonify({"success": True})
except Exception as e:
print(f"Error starting metadata update: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/metadata/stop', methods=['POST'])
def stop_metadata_update():
"""Stop the metadata update process"""
global metadata_update_state
try:
if metadata_update_state['status'] == 'running':
metadata_update_state['status'] = 'stopping'
metadata_update_state['current_artist'] = 'Stopping...'
add_activity_item("⏹️", "Metadata Update", "Stopping metadata update process", "Now")
return jsonify({"success": True})
except Exception as e:
print(f"Error stopping metadata update: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/metadata/status', methods=['GET'])
def get_metadata_update_status():
"""Get current metadata update status"""
try:
# Return a copy of the state with datetime serialization
state_copy = metadata_update_state.copy()
# Convert datetime objects to ISO format for JSON serialization
if state_copy.get('started_at'):
state_copy['started_at'] = state_copy['started_at'].isoformat()
if state_copy.get('completed_at'):
state_copy['completed_at'] = state_copy['completed_at'].isoformat()
return jsonify({"success": True, "status": state_copy})
except Exception as e:
print(f"Error getting metadata update status: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/active-media-server', methods=['GET'])
def get_active_media_server():
"""Get the currently active media server"""
try:
active_server = config_manager.get_active_media_server()
return jsonify({"success": True, "active_server": active_server})
except Exception as e:
print(f"Error getting active media server: {e}")
return jsonify({"success": False, "error": str(e)}), 500
class WebMetadataUpdateWorker:
"""Web-based metadata update worker - EXACT port of dashboard.py MetadataUpdateWorker"""
def __init__(self, artists, media_client, spotify_client, server_type, refresh_interval_days=30):
self.artists = artists
self.media_client = media_client # Can be plex_client or jellyfin_client
self.spotify_client = spotify_client
self.server_type = server_type # "plex" or "jellyfin"
self.matching_engine = MusicMatchingEngine()
self.refresh_interval_days = refresh_interval_days
self.should_stop = False
self.processed_count = 0
self.successful_count = 0
self.failed_count = 0
self.max_workers = 4
self.thread_lock = threading.Lock()
def stop(self):
self.should_stop = True
def get_artist_name(self, artist):
"""Get artist name consistently across Plex and Jellyfin"""
return getattr(artist, 'title', 'Unknown Artist')
def run(self):
"""Process all artists one by one - EXACT copy from dashboard.py"""
global metadata_update_state
try:
# Load artists in background if not provided - EXACTLY like dashboard.py
if self.artists is None:
# Enable lightweight mode for Jellyfin to skip track caching
if self.server_type == "jellyfin":
self.media_client.set_metadata_only_mode(True)
all_artists = self.media_client.get_all_artists()
print(f"[DEBUG] Raw artists returned: {[getattr(a, 'title', 'NO_TITLE') for a in (all_artists or [])]}")
if not all_artists:
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = f"No artists found in {self.server_type.title()} library"
add_activity_item("", "Metadata Update", metadata_update_state['error'], "Now")
return
# Filter artists that need processing
artists_to_process = [artist for artist in all_artists if self.artist_needs_processing(artist)]
self.artists = artists_to_process
# Emit loaded signal equivalent - EXACTLY like dashboard.py
if len(artists_to_process) == 0:
metadata_update_state['status'] = 'completed'
metadata_update_state['completed_at'] = datetime.now()
add_activity_item("", "Metadata Update", "All artists already have good metadata", "Now")
return
else:
add_activity_item("🎵", "Metadata Update", f"Processing {len(artists_to_process)} of {len(all_artists)} artists", "Now")
if not artists_to_process:
metadata_update_state['status'] = 'completed'
metadata_update_state['completed_at'] = datetime.now()
return
total_artists = len(self.artists)
metadata_update_state['total'] = total_artists
# Process artists in parallel using ThreadPoolExecutor - EXACTLY like dashboard.py
def process_single_artist(artist):
"""Process a single artist and return results"""
if self.should_stop or metadata_update_state['status'] == 'stopping':
return None
artist_name = getattr(artist, 'title', 'Unknown Artist')
# Double-check ignore flag right before processing
if self.media_client.is_artist_ignored(artist):
return (artist_name, True, "Skipped (ignored)")
try:
success, details = self.update_artist_metadata(artist)
return (artist_name, success, details)
except Exception as e:
return (artist_name, False, f"Error: {str(e)}")
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# Submit all tasks
future_to_artist = {executor.submit(process_single_artist, artist): artist
for artist in self.artists}
# Process completed tasks as they finish
for future in as_completed(future_to_artist):
if self.should_stop or metadata_update_state['status'] == 'stopping':
break
result = future.result()
if result is None: # Task was cancelled
continue
artist_name, success, details = result
with self.thread_lock:
self.processed_count += 1
if success:
self.successful_count += 1
else:
self.failed_count += 1
# Update global state - equivalent to progress_updated.emit
progress_percent = (self.processed_count / total_artists) * 100
metadata_update_state.update({
'current_artist': artist_name,
'processed': self.processed_count,
'percentage': progress_percent,
'successful': self.successful_count,
'failed': self.failed_count
})
# Individual artist updates are tracked in progress but not shown as separate activity items
# This prevents spam in the activity feed (unlike dashboard which shows these in a separate widget)
# Mark as completed - equivalent to finished.emit
metadata_update_state['status'] = 'completed'
metadata_update_state['completed_at'] = datetime.now()
metadata_update_state['current_artist'] = 'Completed'
summary = f"Processed {self.processed_count} artists: {self.successful_count} updated, {self.failed_count} failed"
add_activity_item("🎵", "Metadata Complete", summary, "Now")
except Exception as e:
print(f"Metadata update failed: {e}")
metadata_update_state['status'] = 'error'
metadata_update_state['error'] = str(e)
add_activity_item("", "Metadata Error", str(e), "Now")
def artist_needs_processing(self, artist):
"""Check if an artist needs metadata processing using age-based detection - EXACT copy from dashboard.py"""
try:
# Check if artist is manually ignored
if self.media_client.is_artist_ignored(artist):
return False
# Use media client's age-based checking with configured interval
return self.media_client.needs_update_by_age(artist, self.refresh_interval_days)
except Exception as e:
print(f"Error checking artist {getattr(artist, 'title', 'Unknown')}: {e}")
return True # Process if we can't determine status
def update_artist_metadata(self, artist):
"""Update a single artist's metadata - EXACT copy from dashboard.py"""
try:
artist_name = getattr(artist, 'title', 'Unknown Artist')
# 1. Search for top 5 potential artists on Spotify
spotify_artists = self.spotify_client.search_artists(artist_name, limit=5)
if not spotify_artists:
return False, "Not found on Spotify"
# 2. Find the best match using the matching engine
best_match = None
highest_score = 0.0
plex_artist_normalized = self.matching_engine.normalize_string(artist_name)
for spotify_artist in spotify_artists:
spotify_artist_normalized = self.matching_engine.normalize_string(spotify_artist.name)
score = self.matching_engine.similarity_score(plex_artist_normalized, spotify_artist_normalized)
if score > highest_score:
highest_score = score
best_match = spotify_artist
# 3. If no suitable match is found, exit
if not best_match or highest_score < 0.7: # Confidence threshold
return False, f"No confident match found (best: '{getattr(best_match, 'name', 'N/A')}', score: {highest_score:.2f})"
spotify_artist = best_match
changes_made = []
# Update photo if needed
photo_updated = self.update_artist_photo(artist, spotify_artist)
if photo_updated:
changes_made.append("photo")
# Update genres
genres_updated = self.update_artist_genres(artist, spotify_artist)
if genres_updated:
changes_made.append("genres")
# Update album artwork (only for Plex, skip for Jellyfin due to API issues)
if self.server_type == "plex":
albums_updated = self.update_album_artwork(artist, spotify_artist)
if albums_updated > 0:
changes_made.append(f"{albums_updated} album art")
else:
# Skip album artwork for Jellyfin until API issues are resolved
print(f"Skipping album artwork updates for Jellyfin artist: {artist.title}")
if changes_made:
# Update artist biography with timestamp to track last update
biography_updated = self.media_client.update_artist_biography(artist)
if biography_updated:
changes_made.append("timestamp")
details = f"Updated {', '.join(changes_made)} (match: '{spotify_artist.name}', score: {highest_score:.2f})"
return True, details
else:
# Even if no metadata changes, update biography to record we checked this artist
self.media_client.update_artist_biography(artist)
return True, "Already up to date"
except Exception as e:
return False, str(e)
def update_artist_photo(self, artist, spotify_artist):
"""Update artist photo from Spotify - EXACT copy from dashboard.py"""
try:
# Check if artist already has a good photo
if self.artist_has_valid_photo(artist):
return False
# Get the image URL from Spotify
if not spotify_artist.image_url:
return False
image_url = spotify_artist.image_url
# Download and validate image
response = requests.get(image_url, timeout=10)
response.raise_for_status()
# Validate and convert image
image_data = self.validate_and_convert_image(response.content)
if not image_data:
return False
# Upload to media server
return self.upload_artist_poster(artist, image_data)
except Exception as e:
print(f"Error updating photo for {getattr(artist, 'title', 'Unknown')}: {e}")
return False
def update_artist_genres(self, artist, spotify_artist):
"""Update artist genres from Spotify and albums - EXACT copy from dashboard.py"""
try:
# Get existing genres
existing_genres = set(genre.tag if hasattr(genre, 'tag') else str(genre)
for genre in (artist.genres or []))
# Get Spotify artist genres
spotify_genres = set(spotify_artist.genres or [])
# Get genres from all albums
album_genres = set()
try:
for album in artist.albums():
if hasattr(album, 'genres') and album.genres:
album_genres.update(genre.tag if hasattr(genre, 'tag') else str(genre)
for genre in album.genres)
except Exception:
pass # Albums might not be accessible
# Combine all genres (prioritize Spotify genres)
all_genres = spotify_genres.union(album_genres)
# Filter out empty/invalid genres
all_genres = {g for g in all_genres if g and g.strip() and len(g.strip()) > 1}
# Only update if we have new genres and they're different
if all_genres and (not existing_genres or all_genres != existing_genres):
# Convert to list and limit to 10 genres
genre_list = list(all_genres)[:10]
# Use media client API to update genres
success = self.media_client.update_artist_genres(artist, genre_list)
if success:
return True
else:
return False
else:
return False
except Exception as e:
print(f"Error updating genres for {getattr(artist, 'title', 'Unknown')}: {e}")
return False
def update_album_artwork(self, artist, spotify_artist):
"""Update album artwork for all albums by this artist - EXACT copy from dashboard.py"""
try:
updated_count = 0
skipped_count = 0
# Get all albums for this artist
try:
albums = list(artist.albums())
except Exception:
print(f"Could not access albums for artist '{artist.title}'")
return 0
if not albums:
print(f"No albums found for artist '{artist.title}'")
return 0
for album in albums:
try:
album_title = getattr(album, 'title', 'Unknown Album')
# Check if album already has good artwork
if self.album_has_valid_artwork(album):
skipped_count += 1
continue
# Search for this specific album on Spotify
album_query = f"album:{album_title} artist:{spotify_artist.name}"
spotify_albums = self.spotify_client.search_albums(album_query, limit=3)
if not spotify_albums:
continue
# Find the best matching album
best_album = None
highest_score = 0.0
plex_album_normalized = self.matching_engine.normalize_string(album_title)
for spotify_album in spotify_albums:
spotify_album_normalized = self.matching_engine.normalize_string(spotify_album.name)
score = self.matching_engine.similarity_score(plex_album_normalized, spotify_album_normalized)
if score > highest_score:
highest_score = score
best_album = spotify_album
# If we found a good match with artwork, download it
if best_album and highest_score > 0.7 and best_album.image_url:
# Download and upload the artwork
if self.download_and_upload_album_artwork(album, best_album.image_url):
updated_count += 1
except Exception as e:
print(f"Error processing album '{getattr(album, 'title', 'Unknown')}': {e}")
continue
return updated_count
except Exception as e:
print(f"Error updating album artwork for artist '{getattr(artist, 'title', 'Unknown')}': {e}")
return 0
def album_has_valid_artwork(self, album):
"""Check if album has valid artwork - EXACT copy from dashboard.py"""
try:
if not hasattr(album, 'thumb') or not album.thumb:
return False
thumb_url = str(album.thumb)
# Completely empty or None
if not thumb_url or thumb_url.strip() == '':
return False
# Obvious placeholder text in URL
obvious_placeholders = ['no-image', 'placeholder', 'missing', 'default-album', 'blank.jpg', 'empty.png']
thumb_lower = thumb_url.lower()
for placeholder in obvious_placeholders:
if placeholder in thumb_lower:
return False
# Extremely short URLs (likely broken)
if len(thumb_url) < 20:
return False
return True
except Exception as e:
return True
def download_and_upload_album_artwork(self, album, image_url):
"""Download artwork from Spotify and upload to media server - EXACT copy from dashboard.py"""
try:
# Download image from Spotify
response = requests.get(image_url, timeout=10)
response.raise_for_status()
# Validate and convert image
image_data = self.validate_and_convert_image(response.content)
if not image_data:
return False
# Upload using media client
success = self.media_client.update_album_poster(album, image_data)
return success
except Exception as e:
print(f"Error downloading/uploading artwork for album '{getattr(album, 'title', 'Unknown')}': {e}")
return False
def artist_has_valid_photo(self, artist):
"""Check if artist has a valid photo - EXACT copy from dashboard.py"""
try:
if not hasattr(artist, 'thumb') or not artist.thumb:
return False
thumb_url = str(artist.thumb)
if 'default' in thumb_url.lower() or len(thumb_url) < 50:
return False
return True
except Exception:
return False
def validate_and_convert_image(self, image_data):
"""Validate and convert image for media server compatibility - EXACT copy from dashboard.py"""
try:
from PIL import Image
import io
# Open and validate image
image = Image.open(io.BytesIO(image_data))
# Check minimum dimensions
width, height = image.size
if width < 200 or height < 200:
return None
# Convert to JPEG for consistency
if image.format != 'JPEG':
buffer = io.BytesIO()
image.convert('RGB').save(buffer, format='JPEG', quality=95)
return buffer.getvalue()
return image_data
except Exception:
return None
def upload_artist_poster(self, artist, image_data):
"""Upload poster using media client - EXACT copy from dashboard.py"""
try:
# Use media client's update method if available
if hasattr(self.media_client, 'update_artist_poster'):
return self.media_client.update_artist_poster(artist, image_data)
# Fallback for Plex: direct API call
if self.server_type == "plex":
import requests
server = self.media_client.server
upload_url = f"{server._baseurl}/library/metadata/{artist.ratingKey}/posters"
headers = {
'X-Plex-Token': server._token,
'Content-Type': 'image/jpeg'
}
response = requests.post(upload_url, data=image_data, headers=headers)
response.raise_for_status()
# Refresh artist to see changes
artist.refresh()
return True
else:
# For other server types, return False since we only have fallback for Plex
return False
except Exception as e:
print(f"Error uploading poster: {e}")
return False
# --- Main Execution ---
if __name__ == '__main__':

View file

@ -6859,6 +6859,18 @@ async function loadDashboardData() {
updateButton.addEventListener('click', handleDbUpdateButtonClick);
}
// Attach event listeners for the metadata updater tool
const metadataButton = document.getElementById('metadata-update-button');
if (metadataButton) {
metadataButton.addEventListener('click', handleMetadataUpdateButtonClick);
}
// Check active media server and hide metadata updater if Jellyfin
await checkAndHideMetadataUpdaterForJellyfin();
// Check for ongoing metadata update and restore state
await checkAndRestoreMetadataUpdateState();
// Attach event listener for the wishlist button
const wishlistButton = document.getElementById('wishlist-button');
if (wishlistButton) {
@ -12665,5 +12677,242 @@ async function removeFromWatchlistModal(artistId, artistName) {
}
// --- Metadata Updater Functions ---
// Global state for metadata update polling
let metadataUpdatePolling = false;
let metadataUpdateInterval = null;
/**
* Handle metadata update button click
*/
async function handleMetadataUpdateButtonClick() {
const button = document.getElementById('metadata-update-button');
const currentAction = button.textContent;
if (currentAction === 'Begin Update') {
// Get refresh interval from dropdown
const refreshSelect = document.getElementById('metadata-refresh-interval');
const refreshIntervalDays = refreshSelect.value !== undefined ? parseInt(refreshSelect.value) : 30;
try {
button.disabled = true;
button.textContent = 'Starting...';
const response = await fetch('/api/metadata/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_interval_days: refreshIntervalDays })
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to start metadata update');
}
showToast('Metadata update started!', 'success');
// Start polling for status updates
startMetadataUpdatePolling();
} catch (error) {
console.error('Error starting metadata update:', error);
button.disabled = false;
button.textContent = 'Begin Update';
showToast(`Error: ${error.message}`, 'error');
}
} else {
// Stop metadata update
try {
button.disabled = true;
button.textContent = 'Stopping...';
const response = await fetch('/api/metadata/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('Failed to stop metadata update');
}
} catch (error) {
console.error('Error stopping metadata update:', error);
button.disabled = false;
button.textContent = 'Stop Update';
}
}
}
/**
* Start polling for metadata update status
*/
function startMetadataUpdatePolling() {
if (metadataUpdatePolling) return; // Already polling
metadataUpdatePolling = true;
metadataUpdateInterval = setInterval(checkMetadataUpdateStatus, 1000); // Poll every second
// Also check immediately
checkMetadataUpdateStatus();
}
/**
* Stop polling for metadata update status
*/
function stopMetadataUpdatePolling() {
metadataUpdatePolling = false;
if (metadataUpdateInterval) {
clearInterval(metadataUpdateInterval);
metadataUpdateInterval = null;
}
}
/**
* Check current metadata update status and update UI
*/
async function checkMetadataUpdateStatus() {
try {
const response = await fetch('/api/metadata/status');
const data = await response.json();
if (data.success && data.status) {
updateMetadataProgressUI(data.status);
// Stop polling if completed or error
if (data.status.status === 'completed' || data.status.status === 'error') {
stopMetadataUpdatePolling();
}
}
} catch (error) {
console.warn('Could not fetch metadata update status:', error);
}
}
/**
* Update metadata progress UI elements
*/
function updateMetadataProgressUI(status) {
const button = document.getElementById('metadata-update-button');
const phaseLabel = document.getElementById('metadata-phase-label');
const progressLabel = document.getElementById('metadata-progress-label');
const progressBar = document.getElementById('metadata-progress-bar');
const refreshSelect = document.getElementById('metadata-refresh-interval');
if (!button || !phaseLabel || !progressLabel || !progressBar || !refreshSelect) return;
if (status.status === 'running') {
button.textContent = 'Stop Update';
button.disabled = false;
refreshSelect.disabled = true;
// Update current artist display
const currentArtist = status.current_artist || 'Processing...';
phaseLabel.textContent = `Current Artist: ${currentArtist}`;
// Update progress
const processed = status.processed || 0;
const total = status.total || 0;
const percentage = status.percentage || 0;
progressLabel.textContent = `${processed} / ${total} artists (${percentage.toFixed(1)}%)`;
progressBar.style.width = `${percentage}%`;
} else if (status.status === 'stopping') {
button.textContent = 'Stopping...';
button.disabled = true;
phaseLabel.textContent = 'Current Artist: Stopping...';
} else if (status.status === 'completed') {
button.textContent = 'Begin Update';
button.disabled = false;
refreshSelect.disabled = false;
phaseLabel.textContent = 'Current Artist: Completed';
const processed = status.processed || 0;
const successful = status.successful || 0;
const failed = status.failed || 0;
progressLabel.textContent = `Completed: ${processed} processed, ${successful} successful, ${failed} failed`;
progressBar.style.width = '100%';
showToast(`Metadata update completed: ${successful} artists updated, ${failed} failed`, 'success');
} else if (status.status === 'error') {
button.textContent = 'Begin Update';
button.disabled = false;
refreshSelect.disabled = false;
phaseLabel.textContent = 'Current Artist: Error occurred';
progressLabel.textContent = status.error || 'Unknown error';
progressBar.style.width = '0%';
} else {
// Idle state
button.textContent = 'Begin Update';
button.disabled = false;
refreshSelect.disabled = false;
phaseLabel.textContent = 'Current Artist: Not running';
progressLabel.textContent = '0 / 0 artists (0.0%)';
progressBar.style.width = '0%';
}
}
/**
* Check active media server and hide metadata updater if Jellyfin
*/
async function checkAndHideMetadataUpdaterForJellyfin() {
try {
const response = await fetch('/api/active-media-server');
const data = await response.json();
if (data.success) {
const metadataCard = document.getElementById('metadata-updater-card');
if (metadataCard) {
if (data.active_server === 'jellyfin') {
// Hide metadata updater for Jellyfin (same as dashboard.py behavior)
metadataCard.style.display = 'none';
console.log('Metadata updater hidden: Jellyfin is active server');
} else {
// Show metadata updater for Plex
metadataCard.style.display = 'block';
console.log('Metadata updater shown: Plex is active server');
}
}
}
} catch (error) {
console.warn('Could not check active media server for metadata updater visibility:', error);
}
}
/**
* Check for ongoing metadata update and restore state on page load
*/
async function checkAndRestoreMetadataUpdateState() {
try {
const response = await fetch('/api/metadata/status');
const data = await response.json();
if (data.success && data.status) {
const status = data.status;
// If metadata update is running, restore the UI state and start polling
if (status.status === 'running') {
console.log('Found ongoing metadata update, restoring state...');
updateMetadataProgressUI(status);
startMetadataUpdatePolling();
} else if (status.status === 'completed' || status.status === 'error') {
// Show final state but don't start polling
updateMetadataProgressUI(status);
}
}
} catch (error) {
console.warn('Could not check metadata update state on page load:', error);
}
}
// --- Global Cleanup on Page Unload ---
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed