sync working
This commit is contained in:
parent
3b0a626b6e
commit
628e8b7709
3 changed files with 655 additions and 15 deletions
322
web_server.py
322
web_server.py
|
|
@ -18,7 +18,7 @@ from flask import Flask, render_template, request, jsonify, redirect, send_file
|
|||
# --- Core Application Imports ---
|
||||
# Import the same core clients and config manager used by the GUI app
|
||||
from config.settings import config_manager
|
||||
from core.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist
|
||||
from core.spotify_client import SpotifyClient, Playlist as SpotifyPlaylist, Track as SpotifyTrack
|
||||
from core.plex_client import PlexClient
|
||||
from core.jellyfin_client import JellyfinClient
|
||||
from core.soulseek_client import SoulseekClient
|
||||
|
|
@ -97,13 +97,13 @@ db_update_state = {
|
|||
"total": 0,
|
||||
"error_message": ""
|
||||
}
|
||||
db_update_lock = threading.Lock()
|
||||
|
||||
# --- Add these globals for the Sync Page ---
|
||||
# --- Sync Page Globals ---
|
||||
sync_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="SyncWorker")
|
||||
active_sync_workers = {} # Key: playlist_id, Value: Future object
|
||||
sync_states = {} # Key: playlist_id, Value: dict with progress info
|
||||
sync_lock = threading.Lock()
|
||||
db_update_lock = threading.Lock()
|
||||
|
||||
# --- Global Matched Downloads Context Management ---
|
||||
# Thread-safe storage for matched download contexts
|
||||
|
|
@ -2455,12 +2455,326 @@ def get_playlist_tracks(playlist_id):
|
|||
'track_count': full_playlist.total_tracks,
|
||||
'image_url': getattr(full_playlist, 'image_url', None),
|
||||
'snapshot_id': getattr(full_playlist, 'snapshot_id', ''),
|
||||
'tracks': [{'name': t.name, 'artists': t.artists, 'album': t.album, 'duration_ms': t.duration_ms} for t in full_playlist.tracks]
|
||||
'tracks': [{'id': t.id, 'name': t.name, 'artists': t.artists, 'album': t.album, 'duration_ms': t.duration_ms, 'popularity': t.popularity} for t in full_playlist.tracks]
|
||||
}
|
||||
return jsonify(playlist_dict)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# Add these new endpoints to the end of web_server.py
|
||||
|
||||
def _run_sync_task(playlist_id, playlist_name, tracks_json):
|
||||
"""The actual sync function that runs in the background thread."""
|
||||
global sync_states, sync_service
|
||||
|
||||
print(f"🚀 _run_sync_task STARTED for playlist '{playlist_name}' (ID: {playlist_id})")
|
||||
print(f"📊 Received {len(tracks_json)} tracks from frontend")
|
||||
|
||||
try:
|
||||
# Recreate a Playlist object from the JSON data sent by the frontend
|
||||
# This avoids needing to re-fetch it from Spotify
|
||||
print(f"🔄 Converting JSON tracks to SpotifyTrack objects...")
|
||||
tracks = []
|
||||
for i, t in enumerate(tracks_json):
|
||||
# Create SpotifyTrack objects with proper default values for missing fields
|
||||
track = SpotifyTrack(
|
||||
id=t.get('id', ''), # Provide default empty string
|
||||
name=t.get('name', ''),
|
||||
artists=t.get('artists', []),
|
||||
album=t.get('album', ''),
|
||||
duration_ms=t.get('duration_ms', 0),
|
||||
popularity=t.get('popularity', 0), # Default value
|
||||
preview_url=t.get('preview_url'),
|
||||
external_urls=t.get('external_urls')
|
||||
)
|
||||
tracks.append(track)
|
||||
if i < 3: # Log first 3 tracks for debugging
|
||||
print(f" Track {i+1}: '{track.name}' by {track.artists}")
|
||||
|
||||
print(f"✅ Created {len(tracks)} SpotifyTrack objects")
|
||||
|
||||
playlist = SpotifyPlaylist(
|
||||
id=playlist_id,
|
||||
name=playlist_name,
|
||||
description=None, # Not needed for sync
|
||||
owner="web_user", # Placeholder
|
||||
public=False, # Default
|
||||
collaborative=False, # Default
|
||||
tracks=tracks,
|
||||
total_tracks=len(tracks)
|
||||
)
|
||||
print(f"✅ Created SpotifyPlaylist object: '{playlist.name}' with {playlist.total_tracks} tracks")
|
||||
|
||||
def progress_callback(progress):
|
||||
"""Callback to update the shared state."""
|
||||
print(f"⚡ PROGRESS CALLBACK: {progress.current_step} - {progress.current_track}")
|
||||
print(f" 📊 Progress: {progress.progress}% ({progress.matched_tracks}/{progress.total_tracks} matched, {progress.failed_tracks} failed)")
|
||||
|
||||
with sync_lock:
|
||||
sync_states[playlist_id] = {
|
||||
"status": "syncing",
|
||||
"progress": progress.__dict__ # Convert dataclass to dict
|
||||
}
|
||||
print(f" ✅ Updated sync_states for {playlist_id}")
|
||||
|
||||
except Exception as setup_error:
|
||||
print(f"❌ SETUP ERROR in _run_sync_task: {setup_error}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
with sync_lock:
|
||||
sync_states[playlist_id] = {
|
||||
"status": "error",
|
||||
"error": f"Setup error: {str(setup_error)}"
|
||||
}
|
||||
return
|
||||
|
||||
try:
|
||||
print(f"🔧 Setting up sync service...")
|
||||
print(f" sync_service available: {sync_service is not None}")
|
||||
|
||||
if sync_service is None:
|
||||
raise Exception("sync_service is None - not initialized properly")
|
||||
|
||||
# Check sync service components
|
||||
print(f" spotify_client: {sync_service.spotify_client is not None}")
|
||||
print(f" plex_client: {sync_service.plex_client is not None}")
|
||||
print(f" jellyfin_client: {sync_service.jellyfin_client is not None}")
|
||||
|
||||
# Check media server connection before starting
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
print(f" Active media server: {active_server}")
|
||||
|
||||
media_client, server_type = sync_service._get_active_media_client()
|
||||
print(f" Media client available: {media_client is not None}")
|
||||
|
||||
if media_client:
|
||||
is_connected = media_client.is_connected()
|
||||
print(f" Media client connected: {is_connected}")
|
||||
|
||||
# Check database access
|
||||
try:
|
||||
from database.music_database import MusicDatabase
|
||||
db = MusicDatabase()
|
||||
print(f" Database initialized: {db is not None}")
|
||||
except Exception as db_error:
|
||||
print(f" ❌ Database initialization failed: {db_error}")
|
||||
|
||||
print(f"🔄 Attaching progress callback...")
|
||||
# Attach the progress callback
|
||||
sync_service.set_progress_callback(progress_callback, playlist.name)
|
||||
print(f"✅ Progress callback attached for playlist: {playlist.name}")
|
||||
|
||||
# CRITICAL FIX: Add database-only fallback for web context
|
||||
# If media client is not connected, patch the sync service to use database-only matching
|
||||
if media_client is None or not media_client.is_connected():
|
||||
print(f"⚠️ Media client not connected - patching sync service for database-only matching")
|
||||
|
||||
# Store original method
|
||||
original_find_track = sync_service._find_track_in_media_server
|
||||
|
||||
# Create database-only replacement method
|
||||
async def database_only_find_track(spotify_track):
|
||||
print(f"🗃️ Database-only search for: '{spotify_track.name}' by {spotify_track.artists}")
|
||||
try:
|
||||
from database.music_database import MusicDatabase
|
||||
from config.settings import config_manager
|
||||
|
||||
db = MusicDatabase()
|
||||
active_server = config_manager.get_active_media_server()
|
||||
original_title = spotify_track.name
|
||||
|
||||
# Try each artist (same logic as original)
|
||||
for artist in spotify_track.artists:
|
||||
artist_name = artist if isinstance(artist, str) else str(artist)
|
||||
|
||||
db_track, confidence = db.check_track_exists(
|
||||
original_title, artist_name,
|
||||
confidence_threshold=0.7,
|
||||
server_source=active_server
|
||||
)
|
||||
|
||||
if db_track and confidence >= 0.7:
|
||||
print(f"✅ Database match: '{db_track.title}' (confidence: {confidence:.2f})")
|
||||
|
||||
# Create mock track object for playlist creation
|
||||
class DatabaseTrackMock:
|
||||
def __init__(self, db_track):
|
||||
self.ratingKey = db_track.id
|
||||
self.title = db_track.title
|
||||
self.id = db_track.id
|
||||
# Add any other attributes needed for playlist creation
|
||||
|
||||
return DatabaseTrackMock(db_track), confidence
|
||||
|
||||
print(f"❌ No database match found for: '{original_title}'")
|
||||
return None, 0.0
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Database search error: {e}")
|
||||
return None, 0.0
|
||||
|
||||
# Patch the method
|
||||
sync_service._find_track_in_media_server = database_only_find_track
|
||||
print(f"✅ Patched sync service to use database-only matching")
|
||||
|
||||
print(f"🚀 Starting actual sync process with asyncio.run()...")
|
||||
# Run the sync (this is a blocking call within this thread)
|
||||
result = asyncio.run(sync_service.sync_playlist(playlist, download_missing=False))
|
||||
print(f"✅ Sync process completed! Result type: {type(result)}")
|
||||
print(f" Result details: matched={getattr(result, 'matched_tracks', 'N/A')}, total={getattr(result, 'total_tracks', 'N/A')}")
|
||||
|
||||
# Update final state on completion
|
||||
with sync_lock:
|
||||
sync_states[playlist_id] = {
|
||||
"status": "finished",
|
||||
"result": result.__dict__ # Convert dataclass to dict
|
||||
}
|
||||
print(f"🏁 Sync finished for {playlist_id} - state updated")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ SYNC FAILED for {playlist_id}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
with sync_lock:
|
||||
sync_states[playlist_id] = {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
finally:
|
||||
print(f"🧹 Cleaning up progress callback for {playlist.name}")
|
||||
# Clean up the callback
|
||||
if sync_service:
|
||||
sync_service.clear_progress_callback(playlist.name)
|
||||
print(f"✅ Cleanup completed for {playlist_id}")
|
||||
|
||||
|
||||
@app.route('/api/sync/start', methods=['POST'])
|
||||
def start_playlist_sync():
|
||||
"""Starts a new sync process for a given playlist."""
|
||||
data = request.get_json()
|
||||
playlist_id = data.get('playlist_id')
|
||||
playlist_name = data.get('playlist_name')
|
||||
tracks_json = data.get('tracks') # Pass the full track list
|
||||
|
||||
if not all([playlist_id, playlist_name, tracks_json]):
|
||||
return jsonify({"success": False, "error": "Missing playlist_id, name, or tracks."}), 400
|
||||
|
||||
with sync_lock:
|
||||
if playlist_id in active_sync_workers and not active_sync_workers[playlist_id].done():
|
||||
return jsonify({"success": False, "error": "Sync is already in progress for this playlist."}), 409
|
||||
|
||||
# Initial state
|
||||
sync_states[playlist_id] = {"status": "starting", "progress": {}}
|
||||
|
||||
# Submit the task to the thread pool
|
||||
future = sync_executor.submit(_run_sync_task, playlist_id, playlist_name, tracks_json)
|
||||
active_sync_workers[playlist_id] = future
|
||||
|
||||
return jsonify({"success": True, "message": "Sync started."})
|
||||
|
||||
|
||||
@app.route('/api/sync/status/<playlist_id>', methods=['GET'])
|
||||
def get_sync_status(playlist_id):
|
||||
"""Polls for the status of an ongoing sync."""
|
||||
with sync_lock:
|
||||
state = sync_states.get(playlist_id)
|
||||
if not state:
|
||||
return jsonify({"status": "not_found"}), 404
|
||||
|
||||
# If the task is finished but the state hasn't been updated, check the future
|
||||
if state['status'] not in ['finished', 'error'] and playlist_id in active_sync_workers:
|
||||
if active_sync_workers[playlist_id].done():
|
||||
# The task might have finished between polls, trigger final state update
|
||||
# This is handled by the _run_sync_task itself
|
||||
pass
|
||||
|
||||
return jsonify(state)
|
||||
|
||||
|
||||
@app.route('/api/sync/cancel', methods=['POST'])
|
||||
def cancel_playlist_sync():
|
||||
"""Cancels an ongoing sync process."""
|
||||
data = request.get_json()
|
||||
playlist_id = data.get('playlist_id')
|
||||
|
||||
if not playlist_id:
|
||||
return jsonify({"success": False, "error": "Missing playlist_id."}), 400
|
||||
|
||||
with sync_lock:
|
||||
future = active_sync_workers.get(playlist_id)
|
||||
if not future or future.done():
|
||||
return jsonify({"success": False, "error": "Sync not running or already complete."}), 404
|
||||
|
||||
# The GUI's sync_service has a cancel_sync method. We'll replicate that idea.
|
||||
# Since we can't easily stop the thread, we'll set a flag.
|
||||
# The elegant solution is to have the sync_service check for a cancellation flag.
|
||||
# Your `sync_service.py` already has this logic with `self._cancelled`.
|
||||
sync_service.cancel_sync()
|
||||
|
||||
# We can't guarantee immediate stop, but we can update the state
|
||||
sync_states[playlist_id] = {"status": "cancelled"}
|
||||
|
||||
# It's best practice to let the task finish and clean itself up.
|
||||
# We don't use future.cancel() as it may not work if the task is already running.
|
||||
|
||||
return jsonify({"success": True, "message": "Sync cancellation requested."})
|
||||
|
||||
@app.route('/api/sync/test-database', methods=['GET'])
|
||||
def test_database_access():
|
||||
"""Test endpoint to verify database connectivity for sync operations"""
|
||||
try:
|
||||
print(f"🧪 Testing database access for sync operations...")
|
||||
|
||||
# Test database initialization
|
||||
from database.music_database import MusicDatabase
|
||||
db = MusicDatabase()
|
||||
print(f" ✅ Database initialized: {db is not None}")
|
||||
|
||||
# Test basic database query
|
||||
stats = db.get_database_info_for_server()
|
||||
print(f" ✅ Database stats retrieved: {stats}")
|
||||
|
||||
# Test track existence check (like sync service does)
|
||||
db_track, confidence = db.check_track_exists("test track", "test artist", confidence_threshold=0.7)
|
||||
print(f" ✅ Track existence check works: found={db_track is not None}, confidence={confidence}")
|
||||
|
||||
# Test config manager
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
print(f" ✅ Active media server: {active_server}")
|
||||
|
||||
# Test media clients
|
||||
print(f" Media clients status:")
|
||||
print(f" plex_client: {plex_client is not None}")
|
||||
if plex_client:
|
||||
print(f" plex_client.is_connected(): {plex_client.is_connected()}")
|
||||
print(f" jellyfin_client: {jellyfin_client is not None}")
|
||||
if jellyfin_client:
|
||||
print(f" jellyfin_client.is_connected(): {jellyfin_client.is_connected()}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Database access test successful",
|
||||
"details": {
|
||||
"database_initialized": db is not None,
|
||||
"database_stats": stats,
|
||||
"active_server": active_server,
|
||||
"plex_connected": plex_client.is_connected() if plex_client else False,
|
||||
"jellyfin_connected": jellyfin_client.is_connected() if jellyfin_client else False,
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Database test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "Database access test failed"
|
||||
}), 500
|
||||
|
||||
# --- Main Execution ---
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ let dbUpdateStatusInterval = null;
|
|||
let spotifyPlaylists = [];
|
||||
let selectedPlaylists = new Set();
|
||||
let activeSyncPollers = {}; // Key: playlist_id, Value: intervalId
|
||||
let playlistTrackCache = {}; // Key: playlist_id, Value: tracks array
|
||||
let spotifyPlaylistsLoaded = false;
|
||||
|
||||
// API endpoints
|
||||
|
|
@ -1504,17 +1505,33 @@ function updateSyncActionsUI() {
|
|||
|
||||
async function openPlaylistDetailsModal(event, playlistId) {
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
const playlist = spotifyPlaylists.find(p => p.id === playlistId);
|
||||
if (!playlist) return;
|
||||
|
||||
showLoadingOverlay(`Fetching tracks for ${playlist.name}...`);
|
||||
showLoadingOverlay(`Loading playlist: ${playlist.name}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/spotify/playlist/${playlistId}`);
|
||||
const fullPlaylist = await response.json();
|
||||
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||
|
||||
showPlaylistDetailsModal(fullPlaylist);
|
||||
// --- CACHING LOGIC START ---
|
||||
if (playlistTrackCache[playlistId]) {
|
||||
console.log(`Cache HIT for playlist ${playlistId}. Using cached tracks.`);
|
||||
// Use the cached tracks instead of fetching
|
||||
const fullPlaylist = { ...playlist, tracks: playlistTrackCache[playlistId] };
|
||||
showPlaylistDetailsModal(fullPlaylist);
|
||||
} else {
|
||||
console.log(`Cache MISS for playlist ${playlistId}. Fetching from server...`);
|
||||
// Fetch from the server if not in cache
|
||||
const response = await fetch(`/api/spotify/playlist/${playlistId}`);
|
||||
const fullPlaylist = await response.json();
|
||||
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||
|
||||
// Store the fetched tracks in the cache
|
||||
playlistTrackCache[playlistId] = fullPlaylist.tracks;
|
||||
console.log(`Cached ${fullPlaylist.tracks.length} tracks for playlist ${playlistId}.`);
|
||||
|
||||
showPlaylistDetailsModal(fullPlaylist);
|
||||
}
|
||||
// --- CACHING LOGIC END ---
|
||||
|
||||
} catch (error) {
|
||||
showToast(`Error: ${error.message}`, 'error');
|
||||
|
|
@ -1542,6 +1559,15 @@ function showPlaylistDetailsModal(playlist) {
|
|||
<span class="playlist-track-count">${playlist.track_count} tracks</span>
|
||||
<span class="playlist-owner">by ${escapeHtml(playlist.owner)}</span>
|
||||
</div>
|
||||
<!-- Sync status display (hidden by default, matches GUI) -->
|
||||
<div class="playlist-modal-sync-status" id="modal-sync-status-${playlist.id}" style="display: none;">
|
||||
<span class="sync-stat total-tracks">♪ <span id="modal-total-${playlist.id}">0</span></span>
|
||||
<span class="sync-separator">/</span>
|
||||
<span class="sync-stat matched-tracks">✓ <span id="modal-matched-${playlist.id}">0</span></span>
|
||||
<span class="sync-separator">/</span>
|
||||
<span class="sync-stat failed-tracks">✗ <span id="modal-failed-${playlist.id}">0</span></span>
|
||||
<span class="sync-stat percentage">(<span id="modal-percentage-${playlist.id}">0</span>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="playlist-modal-close" onclick="closePlaylistDetailsModal()">×</span>
|
||||
</div>
|
||||
|
|
@ -1567,7 +1593,7 @@ function showPlaylistDetailsModal(playlist) {
|
|||
|
||||
<div class="playlist-modal-footer">
|
||||
<button class="playlist-modal-btn playlist-modal-btn-secondary" onclick="closePlaylistDetailsModal()">Close</button>
|
||||
<button class="playlist-modal-btn playlist-modal-btn-primary" onclick="startPlaylistSyncFromModal('${playlist.id}')">Sync Playlist</button>
|
||||
<button class="playlist-modal-btn playlist-modal-btn-primary" onclick="startPlaylistSync('${playlist.id}')">Sync Playlist</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1588,11 +1614,242 @@ function formatDuration(ms) {
|
|||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function startPlaylistSyncFromModal(playlistId) {
|
||||
closePlaylistDetailsModal();
|
||||
showToast('Sync functionality will be implemented next!', 'info');
|
||||
// Find and REPLACE the old startPlaylistSyncFromModal function
|
||||
async function startPlaylistSync(playlistId) {
|
||||
console.log(`🚀 Starting sync for playlist: ${playlistId}`);
|
||||
const playlist = spotifyPlaylists.find(p => p.id === playlistId);
|
||||
if (!playlist) {
|
||||
console.error(`❌ Could not find playlist data for ID: ${playlistId}`);
|
||||
showToast('Could not find playlist data.', 'error');
|
||||
return;
|
||||
}
|
||||
console.log(`✅ Found playlist: ${playlist.name} with ${playlist.track_count || 'unknown'} tracks`);
|
||||
|
||||
// Ensure we have the full track list before starting
|
||||
let tracks = playlistTrackCache[playlistId];
|
||||
if (!tracks) {
|
||||
console.log(`🔄 Cache miss - fetching tracks for playlist ${playlistId}`);
|
||||
try {
|
||||
const response = await fetch(`/api/spotify/playlist/${playlistId}`);
|
||||
const fullPlaylist = await response.json();
|
||||
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||
tracks = fullPlaylist.tracks;
|
||||
playlistTrackCache[playlistId] = tracks; // Cache it
|
||||
console.log(`✅ Fetched and cached ${tracks.length} tracks`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to fetch tracks:`, error);
|
||||
showToast(`Failed to fetch tracks for sync: ${error.message}`, 'error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(`✅ Using cached tracks: ${tracks.length} tracks`);
|
||||
}
|
||||
|
||||
// DON'T close the modal - let it show live progress like the GUI
|
||||
|
||||
try {
|
||||
// First test database access
|
||||
console.log(`🧪 Testing database access before sync...`);
|
||||
try {
|
||||
const testResponse = await fetch('/api/sync/test-database');
|
||||
const testData = await testResponse.json();
|
||||
console.log(`🧪 Database test result:`, testData);
|
||||
} catch (testError) {
|
||||
console.warn(`⚠️ Database test failed:`, testError);
|
||||
}
|
||||
|
||||
console.log(`🔄 Making API call to /api/sync/start with ${tracks.length} tracks`);
|
||||
const response = await fetch('/api/sync/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
playlist_id: playlist.id,
|
||||
playlist_name: playlist.name,
|
||||
tracks: tracks // Send the full track list
|
||||
})
|
||||
});
|
||||
|
||||
console.log(`📡 API response status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
console.log(`📡 API response data:`, data);
|
||||
|
||||
if (!data.success) throw new Error(data.error);
|
||||
|
||||
console.log(`✅ Sync started successfully for "${playlist.name}"`);
|
||||
showToast(`Sync started for "${playlist.name}"`, 'success');
|
||||
|
||||
// Show initial sync state in modal if open
|
||||
const modal = document.getElementById('playlist-details-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
const statusDisplay = document.getElementById(`modal-sync-status-${playlist.id}`);
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'flex';
|
||||
console.log(`📊 Showing modal sync status for ${playlist.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateCardToSyncing(playlist.id, 0); // Initial state
|
||||
startSyncPolling(playlist.id);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to start sync:`, error);
|
||||
showToast(`Failed to start sync: ${error.message}`, 'error');
|
||||
updateCardToDefault(playlist.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add these new helper functions to script.js
|
||||
|
||||
function startSyncPolling(playlistId) {
|
||||
// Clear any existing poller for this playlist
|
||||
if (activeSyncPollers[playlistId]) {
|
||||
clearInterval(activeSyncPollers[playlistId]);
|
||||
}
|
||||
|
||||
// Start a new poller that checks every 2 seconds
|
||||
console.log(`🔄 Starting sync polling for playlist: ${playlistId}`);
|
||||
activeSyncPollers[playlistId] = setInterval(async () => {
|
||||
try {
|
||||
console.log(`📊 Polling sync status for: ${playlistId}`);
|
||||
const response = await fetch(`/api/sync/status/${playlistId}`);
|
||||
const state = await response.json();
|
||||
console.log(`📊 Poll response:`, state);
|
||||
|
||||
if (state.status === 'syncing') {
|
||||
const progress = state.progress;
|
||||
console.log(`📊 Sync progress:`, progress);
|
||||
console.log(` 📊 Progress values: ${progress.progress}% | Total: ${progress.total_tracks} | Matched: ${progress.matched_tracks} | Failed: ${progress.failed_tracks}`);
|
||||
console.log(` 📊 Current step: "${progress.current_step}" | Current track: "${progress.current_track}"`);
|
||||
|
||||
// Use the actual progress percentage from the sync service
|
||||
updateCardToSyncing(playlistId, progress.progress, progress);
|
||||
// Also update the modal if it's open
|
||||
updateModalSyncProgress(playlistId, progress);
|
||||
} else if (state.status === 'finished' || state.status === 'error' || state.status === 'cancelled') {
|
||||
console.log(`🏁 Sync completed with status: ${state.status}`);
|
||||
stopSyncPolling(playlistId);
|
||||
updateCardToDefault(playlistId, state);
|
||||
// Also update the modal if it's open
|
||||
closePlaylistDetailsModal(); // Close modal on completion/error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error polling sync status for ${playlistId}:`, error);
|
||||
stopSyncPolling(playlistId);
|
||||
updateCardToDefault(playlistId, { status: 'error', error: 'Polling failed' });
|
||||
}
|
||||
}, 2000); // Poll every 2 seconds
|
||||
}
|
||||
|
||||
function stopSyncPolling(playlistId) {
|
||||
if (activeSyncPollers[playlistId]) {
|
||||
clearInterval(activeSyncPollers[playlistId]);
|
||||
delete activeSyncPollers[playlistId];
|
||||
}
|
||||
}
|
||||
|
||||
function updateCardToSyncing(playlistId, percent, progress = null) {
|
||||
const card = document.querySelector(`.playlist-card[data-playlist-id="${playlistId}"]`);
|
||||
if (!card) return;
|
||||
|
||||
const progressBar = card.querySelector('.sync-progress-indicator');
|
||||
progressBar.style.display = 'block';
|
||||
|
||||
let progressText = 'Starting...';
|
||||
let actualPercent = percent || 0;
|
||||
|
||||
if (progress) {
|
||||
// Use the actual progress percentage from the sync service
|
||||
actualPercent = progress.progress || 0;
|
||||
|
||||
// Create detailed progress text like the GUI
|
||||
const matched = progress.matched_tracks || 0;
|
||||
const failed = progress.failed_tracks || 0;
|
||||
const total = progress.total_tracks || 0;
|
||||
const currentStep = progress.current_step || 'Processing';
|
||||
|
||||
if (total > 0) {
|
||||
const processed = matched + failed;
|
||||
progressText = `${currentStep}: ${processed}/${total} (${matched} matched, ${failed} failed)`;
|
||||
} else {
|
||||
progressText = currentStep;
|
||||
}
|
||||
|
||||
// If there's a current track being processed, show it
|
||||
if (progress.current_track) {
|
||||
progressText += ` - ${progress.current_track}`;
|
||||
}
|
||||
}
|
||||
|
||||
progressBar.innerHTML = `
|
||||
<div class="progress-bar-sync">
|
||||
<div class="progress-fill-sync" style="width: ${actualPercent}%;"></div>
|
||||
</div>
|
||||
<div class="progress-text-sync">${progressText}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateCardToDefault(playlistId, finalState = null) {
|
||||
const card = document.querySelector(`.playlist-card[data-playlist-id="${playlistId}"]`);
|
||||
if (!card) return;
|
||||
|
||||
const progressBar = card.querySelector('.sync-progress-indicator');
|
||||
progressBar.style.display = 'none';
|
||||
progressBar.innerHTML = '';
|
||||
|
||||
const statusEl = card.querySelector('.playlist-card-status');
|
||||
if (finalState) {
|
||||
if (finalState.status === 'finished') {
|
||||
statusEl.textContent = `Synced: Just now`;
|
||||
statusEl.className = 'playlist-card-status status-synced';
|
||||
showToast(`Sync complete for "${card.querySelector('.playlist-card-name').textContent}"`, 'success');
|
||||
} else {
|
||||
statusEl.textContent = `Sync Failed`;
|
||||
statusEl.className = 'playlist-card-status status-needs-sync'; // Or a new error class
|
||||
showToast(`Sync failed: ${finalState.error || 'Unknown error'}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the modal's sync progress display (matches GUI functionality)
|
||||
function updateModalSyncProgress(playlistId, progress) {
|
||||
const modal = document.getElementById('playlist-details-modal');
|
||||
if (modal && modal.style.display !== 'none') {
|
||||
console.log(`📊 Updating modal sync progress for ${playlistId}:`, progress);
|
||||
|
||||
// Show sync status display
|
||||
const statusDisplay = document.getElementById(`modal-sync-status-${playlistId}`);
|
||||
if (statusDisplay) {
|
||||
statusDisplay.style.display = 'flex';
|
||||
|
||||
// Update counters (matching GUI exactly)
|
||||
const totalEl = document.getElementById(`modal-total-${playlistId}`);
|
||||
const matchedEl = document.getElementById(`modal-matched-${playlistId}`);
|
||||
const failedEl = document.getElementById(`modal-failed-${playlistId}`);
|
||||
const percentageEl = document.getElementById(`modal-percentage-${playlistId}`);
|
||||
|
||||
const total = progress.total_tracks || 0;
|
||||
const matched = progress.matched_tracks || 0;
|
||||
const failed = progress.failed_tracks || 0;
|
||||
|
||||
if (totalEl) totalEl.textContent = total;
|
||||
if (matchedEl) matchedEl.textContent = matched;
|
||||
if (failedEl) failedEl.textContent = failed;
|
||||
|
||||
// Calculate percentage like GUI
|
||||
if (total > 0) {
|
||||
const processed = matched + failed;
|
||||
const percentage = Math.round((processed / total) * 100);
|
||||
if (percentageEl) percentageEl.textContent = percentage;
|
||||
}
|
||||
|
||||
console.log(`📊 Modal updated: ♪ ${total} / ✓ ${matched} / ✗ ${failed} (${Math.round((matched + failed) / total * 100)}%)`);
|
||||
} else {
|
||||
console.warn(`❌ Modal sync status display not found for ${playlistId}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`📊 Modal not open for ${playlistId}, skipping update`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Download tracking state management - matching GUI functionality
|
||||
|
|
|
|||
|
|
@ -4465,4 +4465,73 @@ body {
|
|||
.playlist-modal-btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.4);
|
||||
}
|
||||
|
||||
/* Add these styles to the end of style.css */
|
||||
|
||||
.sync-progress-indicator {
|
||||
margin-top: 10px;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
.progress-bar-sync {
|
||||
height: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill-sync {
|
||||
height: 100%;
|
||||
background-color: #1ed760;
|
||||
width: 0%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.progress-text-sync {
|
||||
font-size: 11px;
|
||||
color: #b3b3b3;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Modal sync status display (matches GUI) */
|
||||
.playlist-modal-sync-status {
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sync-stat.total-tracks {
|
||||
color: #ffa500;
|
||||
}
|
||||
|
||||
.sync-stat.matched-tracks {
|
||||
color: #1db954;
|
||||
}
|
||||
|
||||
.sync-stat.failed-tracks {
|
||||
color: #e22134;
|
||||
}
|
||||
|
||||
.sync-stat.percentage {
|
||||
color: #1db954;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sync-separator {
|
||||
color: #666666;
|
||||
}
|
||||
Loading…
Reference in a new issue