This commit is contained in:
Broque Thomas 2025-08-29 14:33:05 -07:00
parent 017d72b38a
commit b88467f949
2 changed files with 373 additions and 4 deletions

View file

@ -3465,6 +3465,67 @@ def get_wishlist_count():
print(f"Error getting wishlist count: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/tracks', methods=['GET'])
def get_wishlist_tracks():
"""Endpoint to get wishlist tracks for display in modal."""
try:
from core.wishlist_service import get_wishlist_service
wishlist_service = get_wishlist_service()
tracks = wishlist_service.get_wishlist_tracks_for_download()
return jsonify({"tracks": tracks})
except Exception as e:
print(f"Error getting wishlist tracks: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/wishlist/download_missing', methods=['POST'])
def start_wishlist_missing_downloads():
"""
This endpoint fetches wishlist tracks and manages them with batch processing
identical to playlist processing, maintaining exactly 3 concurrent downloads.
"""
try:
from core.wishlist_service import get_wishlist_service
wishlist_service = get_wishlist_service()
# Get wishlist tracks formatted for download modal
wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
if not wishlist_tracks:
return jsonify({"success": False, "error": "No tracks in wishlist"}), 400
batch_id = str(uuid.uuid4())
# Use "wishlist" as the playlist_id for consistency in the modal system
playlist_id = "wishlist"
playlist_name = "Wishlist"
# Create task queue for this batch - convert wishlist tracks to the expected format
task_queue = []
with tasks_lock:
download_batches[batch_id] = {
'phase': 'analysis',
'playlist_id': playlist_id,
'playlist_name': playlist_name,
'queue': task_queue,
'active_count': 0,
'max_concurrent': 3,
'queue_index': 0,
'analysis_total': len(wishlist_tracks),
'analysis_processed': 0,
'analysis_results': []
}
# Submit the wishlist processing job using the same processing function
missing_download_executor.submit(_run_full_missing_tracks_process, batch_id, playlist_id, wishlist_tracks)
return jsonify({
"success": True,
"batch_id": batch_id
})
except Exception as e:
print(f"Error starting wishlist download process: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/database/update', methods=['POST'])
def start_database_update():
"""Endpoint to start the database update process."""
@ -3625,7 +3686,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
found, confidence = False, 0.0
for artist in artists:
artist_name = artist if isinstance(artist, str) else str(artist)
# Handle both string format and Spotify API format {'name': 'Artist Name'}
if isinstance(artist, str):
artist_name = artist
elif isinstance(artist, dict) and 'name' in artist:
artist_name = artist['name']
else:
artist_name = str(artist)
db_track, track_confidence = db.check_track_exists(
track_name, artist_name, confidence_threshold=0.7, server_source=active_server
)
@ -3715,11 +3782,31 @@ def _download_track_worker(task_id, batch_id=None):
track_data = task['track_info']
# Recreate a SpotifyTrack object for the matching engine
# Handle both string format and Spotify API format for artists
raw_artists = track_data.get('artists', [])
processed_artists = []
for artist in raw_artists:
if isinstance(artist, str):
processed_artists.append(artist)
elif isinstance(artist, dict) and 'name' in artist:
processed_artists.append(artist['name'])
else:
processed_artists.append(str(artist))
# Handle album field - extract name if it's a dictionary
raw_album = track_data.get('album', '')
if isinstance(raw_album, dict) and 'name' in raw_album:
album_name = raw_album['name']
elif isinstance(raw_album, str):
album_name = raw_album
else:
album_name = str(raw_album)
track = SpotifyTrack(
id=track_data.get('id', ''),
name=track_data.get('name', ''),
artists=track_data.get('artists', []),
album=track_data.get('album', ''),
artists=processed_artists,
album=album_name,
duration_ms=track_data.get('duration_ms', 0),
popularity=track_data.get('popularity', 0)
)

View file

@ -1782,8 +1782,28 @@ async function checkForActiveProcesses() {
async function rehydrateModal(processInfo) {
const { playlist_id, playlist_name, batch_id } = processInfo;
console.log(`💧 Rehydrating modal for playlist "${playlist_name}" (batch: ${batch_id})`);
console.log(`💧 Rehydrating modal for "${playlist_name}" (batch: ${batch_id})`);
// Handle wishlist processes specially
if (playlist_id === "wishlist") {
await openDownloadMissingWishlistModal();
const process = activeDownloadProcesses[playlist_id];
if (!process) return;
process.status = 'running';
process.batchId = batch_id;
updateRefreshButtonState();
document.getElementById(`begin-analysis-btn-${playlist_id}`).style.display = 'none';
document.getElementById(`cancel-all-btn-${playlist_id}`).style.display = 'inline-block';
startModalDownloadPolling(playlist_id);
process.modalElement.style.display = 'none';
return;
}
// Handle regular Spotify playlist processes
let playlistData = spotifyPlaylists.find(p => p.id === playlist_id);
if (!playlistData) {
console.warn(`Cannot rehydrate modal: Playlist data for ${playlist_id} not loaded.`);
@ -2296,6 +2316,210 @@ function closeDownloadMissingModal(playlistId) {
}
}
async function openDownloadMissingWishlistModal() {
const playlistId = "wishlist"; // Use a consistent ID for wishlist
// Check if a process is already active for the wishlist
if (activeDownloadProcesses[playlistId]) {
console.log(`Modal for wishlist already exists. Showing it.`);
const process = activeDownloadProcesses[playlistId];
if (process.modalElement) {
// Show helpful message if it's a completed process
if (process.status === 'complete') {
showToast('Showing previous results. Close this modal to start a new analysis.', 'info');
}
process.modalElement.style.display = 'flex';
}
return; // Don't create a new one
}
console.log(`📥 Opening Download Missing Tracks modal for wishlist`);
// Fetch actual wishlist tracks from the server
let tracks;
try {
const response = await fetch('/api/wishlist/count');
const countData = await response.json();
if (countData.count === 0) {
showToast('Wishlist is empty. No tracks to download.', 'info');
return;
}
// Fetch the actual wishlist tracks for display
const tracksResponse = await fetch('/api/wishlist/tracks');
if (!tracksResponse.ok) {
throw new Error('Failed to fetch wishlist tracks');
}
const tracksData = await tracksResponse.json();
tracks = tracksData.tracks || [];
} catch (error) {
showToast(`Failed to fetch wishlist data: ${error.message}`, 'error');
return;
}
currentPlaylistTracks = tracks;
currentModalPlaylistId = playlistId;
let modal = document.createElement('div');
modal.id = `download-missing-modal-${playlistId}`; // Unique ID
modal.className = 'download-missing-modal'; // Use class for styling
modal.style.display = 'none'; // Start hidden
document.body.appendChild(modal);
// Register the new process in our global state tracker
activeDownloadProcesses[playlistId] = {
status: 'idle', // idle, running, complete, cancelled
modalElement: modal,
poller: null,
batchId: null,
playlist: { id: playlistId, name: "Wishlist" }, // Create a pseudo-playlist object
tracks: tracks
};
modal.innerHTML = `
<div class="download-missing-modal-content">
<div class="download-missing-modal-header">
<h2 class="download-missing-modal-title">Download Missing Tracks - Wishlist</h2>
<span class="download-missing-modal-close" onclick="closeDownloadMissingModal('${playlistId}')">&times;</span>
</div>
<div class="download-missing-modal-body">
<div class="download-dashboard-stats">
<div class="dashboard-stat stat-total">
<div class="dashboard-stat-number" id="stat-total-${playlistId}">${tracks.length}</div>
<div class="dashboard-stat-label">Total Tracks</div>
</div>
<div class="dashboard-stat stat-found">
<div class="dashboard-stat-number" id="stat-found-${playlistId}">-</div>
<div class="dashboard-stat-label">Found in Library</div>
</div>
<div class="dashboard-stat stat-missing">
<div class="dashboard-stat-number" id="stat-missing-${playlistId}">-</div>
<div class="dashboard-stat-label">Missing Tracks</div>
</div>
<div class="dashboard-stat stat-downloaded">
<div class="dashboard-stat-number" id="stat-downloaded-${playlistId}">0</div>
<div class="dashboard-stat-label">Downloaded</div>
</div>
</div>
<div class="download-progress-section">
<div class="progress-item">
<div class="progress-label">
🔍 Library Analysis
<span id="analysis-progress-text-${playlistId}">Ready to start</span>
</div>
<div class="progress-bar">
<div class="progress-fill analysis" id="analysis-progress-fill-${playlistId}"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-label">
Downloads
<span id="download-progress-text-${playlistId}">Waiting for analysis</span>
</div>
<div class="progress-bar">
<div class="progress-fill download" id="download-progress-fill-${playlistId}"></div>
</div>
</div>
</div>
<div class="download-tracks-section">
<div class="download-tracks-header">
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
</div>
<div class="download-tracks-table-container">
<table class="download-tracks-table">
<thead>
<tr>
<th>#</th>
<th>Track</th>
<th>Artist</th>
<th>Library Match</th>
<th>Download Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="download-tracks-tbody-${playlistId}">
${tracks.map((track, index) => `
<tr data-track-index="${index}">
<td class="track-number">${index + 1}</td>
<td class="track-name" title="${escapeHtml(track.name)}">${escapeHtml(track.name)}</td>
<td class="track-artist" title="${escapeHtml(formatArtists(track.artists))}">${formatArtists(track.artists)}</td>
<td class="track-match-status match-checking" id="match-${playlistId}-${index}">🔍 Pending</td>
<td class="track-download-status" id="download-${playlistId}-${index}">-</td>
<td class="track-actions" id="actions-${playlistId}-${index}">-</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
<div class="download-missing-modal-footer">
<div class="download-phase-controls">
<button class="download-control-btn primary" id="begin-analysis-btn-${playlistId}" onclick="startWishlistMissingTracksProcess('${playlistId}')">
Begin Analysis
</button>
<button class="download-control-btn danger" id="cancel-all-btn-${playlistId}" onclick="cancelAllOperations('${playlistId}')" style="display: none;">
Cancel All
</button>
</div>
<div class="modal-close-section">
<button class="download-control-btn secondary" onclick="closeDownloadMissingModal('${playlistId}')">Close</button>
</div>
</div>
</div>
`;
modal.style.display = 'flex';
}
async function startWishlistMissingTracksProcess(playlistId) {
const process = activeDownloadProcesses[playlistId];
if (!process) return;
console.log(`🚀 Kicking off wishlist missing tracks process`);
try {
process.status = 'running';
updateRefreshButtonState();
document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'none';
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'inline-block';
const response = await fetch('/api/wishlist/download_missing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (!data.success) {
// Special handling for rate limit
if (response.status === 429) {
throw new Error(`${data.error} Try closing some other download processes first.`);
}
throw new Error(data.error);
}
process.batchId = data.batch_id;
console.log(`✅ Wishlist process started successfully. Batch ID: ${data.batch_id}`);
// Start polling for updates
startModalDownloadPolling(playlistId);
} catch (error) {
console.error('Error starting wishlist missing tracks process:', error);
showToast(`Error: ${error.message}`, 'error');
// Reset UI state on error
process.status = 'idle';
updateRefreshButtonState();
document.getElementById(`begin-analysis-btn-${playlistId}`).style.display = 'inline-block';
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none';
}
}
async function startMissingTracksProcess(playlistId) {
const process = activeDownloadProcesses[playlistId];
if (!process) return;
@ -2337,6 +2561,7 @@ async function startMissingTracksProcess(playlistId) {
function updateTrackAnalysisResults(playlistId, results) {
// Update match results for all rows (tracks are now pre-populated)
for (const result of results) {
const matchElement = document.getElementById(`match-${playlistId}-${result.track_index}`);
if (matchElement) {
@ -3728,6 +3953,25 @@ function escapeHtml(text) {
return div.innerHTML;
}
function formatArtists(artists) {
if (!artists || !Array.isArray(artists)) {
return 'Unknown Artist';
}
// Handle both string arrays and object arrays with 'name' property
const artistNames = artists.map(artist => {
if (typeof artist === 'string') {
return artist;
} else if (artist && typeof artist === 'object' && artist.name) {
return artist.name;
} else {
return 'Unknown Artist';
}
});
return artistNames.join(', ') || 'Unknown Artist';
}
async function showVersionInfo() {
try {
console.log('Fetching version info...');
@ -4037,6 +4281,15 @@ window.cancelAllOperations = cancelAllOperations;
window.cancelTrackDownload = cancelTrackDownload;
window.handleViewProgressClick = handleViewProgressClick;
// Wishlist Modal functions
window.openDownloadMissingWishlistModal = openDownloadMissingWishlistModal;
window.startWishlistMissingTracksProcess = startWishlistMissingTracksProcess;
window.handleWishlistButtonClick = handleWishlistButtonClick;
// Helper functions
window.escapeHtml = escapeHtml;
window.formatArtists = formatArtists;
// APPEND THIS JAVASCRIPT SNIPPET (B)
@ -4970,6 +5223,12 @@ async function loadDashboardData() {
updateButton.addEventListener('click', handleDbUpdateButtonClick);
}
// Attach event listener for the wishlist button
const wishlistButton = document.getElementById('wishlist-button');
if (wishlistButton) {
wishlistButton.addEventListener('click', handleWishlistButtonClick);
}
// Initial load of stats
await fetchAndUpdateDbStats();
@ -4986,6 +5245,9 @@ async function loadDashboardData() {
// Also check the status of any ongoing update when the page loads
await checkAndUpdateDbProgress();
// Check for any active download processes that need rehydration
await checkForActiveProcesses();
}
// --- Data Fetching and UI Updates ---
@ -5225,4 +5487,24 @@ async function handleDbUpdateButtonClick() {
showToast('Error sending stop request.', 'error');
}
}
}
async function handleWishlistButtonClick() {
try {
// First check if there are any tracks in the wishlist
const response = await fetch('/api/wishlist/count');
const data = await response.json();
if (data.count === 0) {
showToast('Wishlist is empty. No tracks to download.', 'info');
return;
}
// Open the wishlist download modal
await openDownloadMissingWishlistModal();
} catch (error) {
console.error('Error handling wishlist button click:', error);
showToast(`Error opening wishlist: ${error.message}`, 'error');
}
}