KaraokeMerge/web/templates/playlist_validation.html

1002 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playlist Validation - Karaoke Library Cleanup</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
.playlist-card {
transition: all 0.3s ease;
cursor: pointer;
}
.playlist-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.playlist-card.selected {
border-color: #0d6efd;
background-color: #f8f9ff;
}
.status-badge {
font-size: 0.8em;
}
.exact-match {
background-color: #d4edda;
border-color: #c3e6cb;
}
.fuzzy-match {
background-color: #fff3cd;
border-color: #ffeaa7;
}
.missing-song {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.song-item {
transition: all 0.2s ease;
}
.song-item:hover {
background-color: #f8f9fa;
}
.similarity-bar {
height: 4px;
background-color: #e9ecef;
border-radius: 2px;
overflow: hidden;
}
.similarity-fill {
height: 100%;
background: linear-gradient(90deg, #dc3545, #ffc107, #28a745);
transition: width 0.3s ease;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.btn-update-song {
font-size: 0.8em;
padding: 0.25rem 0.5rem;
}
.video-preview {
max-width: 100%;
height: auto;
}
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.progress-ring {
width: 60px;
height: 60px;
}
.progress-ring circle {
fill: none;
stroke-width: 4;
}
.progress-ring .bg {
stroke: rgba(255,255,255,0.2);
}
.progress-ring .progress {
stroke: white;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.batch-update-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #f8f9fa;
border-top: 2px solid #dee2e6;
padding: 12px 15px;
z-index: 1000;
transform: translateY(100%);
transition: transform 0.3s ease;
max-height: 280px;
display: flex;
flex-direction: column;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
.batch-update-panel.show {
transform: translateY(0);
}
.batch-update-panel h5 {
margin: 0 0 10px 0;
font-size: 1.1rem;
}
.queued-changes-container {
flex: 1;
overflow-y: auto;
max-height: 180px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
}
.queued-item {
background: white;
border-bottom: 1px solid #dee2e6;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.queued-item:last-child {
border-bottom: none;
}
.queued-item .change-info {
flex: 1;
font-size: 0.9rem;
}
.queued-item .remove-btn {
margin-left: 10px;
flex-shrink: 0;
}
.queued-song {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
}
.queued-song.duplicate {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
}
.main-content {
margin-bottom: 100px;
}
</style>
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<div class="row bg-primary text-white p-3 mb-4">
<div class="col">
<h1><i class="fas fa-list-check"></i> Playlist Validation</h1>
<p class="mb-0">Validate playlists against your song library and fix fuzzy matches</p>
</div>
</div>
<!-- Navigation -->
<div class="row mb-4">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="fas fa-home"></i> Home</a></li>
<li class="breadcrumb-item active">Playlist Validation</li>
</ol>
</nav>
</div>
</div>
<!-- Main Content -->
<div class="row main-content">
<!-- Playlist Selection -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-music"></i> Playlists</h5>
</div>
<div class="card-body">
<div class="mb-3">
<button id="validateAllBtn" class="btn btn-primary w-100 mb-2">
<i class="fas fa-play"></i> Validate All Playlists
</button>
<button id="refreshPlaylistsBtn" class="btn btn-outline-secondary w-100">
<i class="fas fa-sync-alt"></i> Refresh Playlists
</button>
</div>
<div id="playlistsList" class="list-group">
<!-- Playlists will be loaded here -->
</div>
</div>
</div>
</div>
<!-- Validation Results -->
<div class="col-md-8">
<div id="validationResults" style="display: none;">
<!-- Overall Stats -->
<div class="row mb-4">
<div class="col-12">
<div class="card stats-card">
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="d-flex align-items-center justify-content-center">
<div class="progress-ring me-3">
<svg viewBox="0 0 36 36">
<circle class="bg" cx="18" cy="18" r="16"></circle>
<circle class="progress" cx="18" cy="18" r="16"
stroke-dasharray="100" stroke-dashoffset="0"></circle>
</svg>
</div>
<div>
<h4 id="totalSongs">0</h4>
<small>Total Songs</small>
</div>
</div>
</div>
<div class="col-md-3">
<h4 id="exactMatches" class="text-success">0</h4>
<small>Exact Matches</small>
</div>
<div class="col-md-3">
<h4 id="fuzzyMatches" class="text-warning">0</h4>
<small>Fuzzy Matches</small>
</div>
<div class="col-md-3">
<h4 id="missingSongs" class="text-danger">0</h4>
<small>Missing Songs</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Playlist Details -->
<div class="card">
<div class="card-header">
<h5 id="currentPlaylistTitle">Playlist Validation Results</h5>
</div>
<div class="card-body">
<!-- Tabs -->
<ul class="nav nav-tabs" id="validationTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="fuzzy-tab" data-bs-toggle="tab"
data-bs-target="#fuzzy" type="button" role="tab">
<i class="fas fa-search"></i> Fuzzy Matches
<span id="fuzzyCount" class="badge bg-warning ms-1">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="missing-tab" data-bs-toggle="tab"
data-bs-target="#missing" type="button" role="tab">
<i class="fas fa-times-circle"></i> Missing Songs
<span id="missingCount" class="badge bg-danger ms-1">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="exact-tab" data-bs-toggle="tab"
data-bs-target="#exact" type="button" role="tab">
<i class="fas fa-check-circle"></i> Exact Matches
<span id="exactCount" class="badge bg-success ms-1">0</span>
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3" id="validationTabContent">
<!-- Fuzzy Matches Tab -->
<div class="tab-pane fade show active" id="fuzzy" role="tabpanel">
<div id="fuzzyMatchesList">
<!-- Fuzzy matches will be loaded here -->
</div>
</div>
<!-- Missing Songs Tab -->
<div class="tab-pane fade" id="missing" role="tabpanel">
<div id="missingSongsList">
<!-- Missing songs will be loaded here -->
</div>
</div>
<!-- Exact Matches Tab -->
<div class="tab-pane fade" id="exact" role="tabpanel">
<div id="exactMatchesList">
<!-- Exact matches will be loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div id="loadingState" class="text-center py-5" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Validating playlists...</p>
</div>
<!-- Welcome State -->
<div id="welcomeState" class="text-center py-5">
<i class="fas fa-list-check fa-3x text-muted mb-3"></i>
<h3>Select a Playlist</h3>
<p class="text-muted">Choose a playlist from the left panel to start validation, or validate all playlists at once.</p>
</div>
</div>
</div>
</div>
<!-- Video Preview Modal -->
<div class="modal fade" id="videoModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Video Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<video id="videoPlayer" class="video-preview w-100" controls>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
<!-- Update Song Modal -->
<div class="modal fade" id="updateSongModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update Playlist Song</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="updateSongForm">
<input type="hidden" id="updatePlaylistIndex">
<input type="hidden" id="updateSongPosition">
<div class="mb-3">
<label class="form-label">Current Playlist Song:</label>
<div class="form-control-plaintext" id="currentPlaylistSong"></div>
</div>
<div class="mb-3">
<label class="form-label">Found Song:</label>
<div class="form-control-plaintext" id="foundSongInfo"></div>
</div>
<div class="mb-3">
<label for="newArtist" class="form-label">New Artist:</label>
<input type="text" class="form-control" id="newArtist" required>
</div>
<div class="mb-3">
<label for="newTitle" class="form-label">New Title:</label>
<input type="text" class="form-control" id="newTitle" required>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="dryRunCheck" checked>
<label class="form-check-label" for="dryRunCheck">
Dry Run (preview changes only)
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmUpdateBtn">
<i class="fas fa-save"></i> Update Song
</button>
</div>
</div>
</div>
</div>
<!-- Batch Update Panel -->
<div class="batch-update-panel" id="batchUpdatePanel">
<h5><i class="fas fa-wrench"></i> Batch Updates (<span id="pendingCount">0</span>)</h5>
<div class="queued-changes-container" id="queuedChangesList">
<!-- Changes will be queued here -->
</div>
<div class="mt-2">
<button class="btn btn-sm btn-primary w-100" id="applyAllChangesBtn">
<i class="fas fa-save"></i> Apply All Changes
</button>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Global variables
let currentPlaylistIndex = null;
let currentValidationResults = null;
let allPlaylists = [];
let pendingChanges = []; // New: Queue for batch updates
// Initialize the page
document.addEventListener('DOMContentLoaded', function() {
loadPlaylists();
setupEventListeners();
// Show batch update panel on load
showBatchUpdatePanel(false);
});
function setupEventListeners() {
// Button event listeners
document.getElementById('validateAllBtn').addEventListener('click', validateAllPlaylists);
document.getElementById('refreshPlaylistsBtn').addEventListener('click', loadPlaylists);
document.getElementById('confirmUpdateBtn').addEventListener('click', updatePlaylistSong);
document.getElementById('applyAllChangesBtn').addEventListener('click', applyAllPendingChanges);
}
async function loadPlaylists() {
try {
const response = await fetch('/api/playlists');
if (!response.ok) throw new Error('Failed to load playlists');
allPlaylists = await response.json();
displayPlaylists(allPlaylists);
} catch (error) {
console.error('Error loading playlists:', error);
showAlert('Error loading playlists: ' + error.message, 'danger');
}
}
function displayPlaylists(playlists) {
const container = document.getElementById('playlistsList');
container.innerHTML = '';
playlists.forEach((playlist, index) => {
const playlistCard = document.createElement('div');
playlistCard.className = 'list-group-item list-group-item-action playlist-card';
playlistCard.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${playlist.title}</h6>
<small class="text-muted">${playlist.song_count} songs</small>
</div>
<button class="btn btn-sm btn-outline-primary validate-playlist-btn"
data-playlist-index="${playlist.index}">
<i class="fas fa-search"></i>
</button>
</div>
`;
playlistCard.addEventListener('click', () => validatePlaylist(playlist.index));
container.appendChild(playlistCard);
});
}
async function validatePlaylist(playlistIndex) {
showLoading(true);
currentPlaylistIndex = playlistIndex;
try {
const response = await fetch(`/api/validate-playlist/${playlistIndex}`);
if (!response.ok) throw new Error('Failed to validate playlist');
const results = await response.json();
currentValidationResults = results;
displayValidationResults(results);
} catch (error) {
console.error('Error validating playlist:', error);
showAlert('Error validating playlist: ' + error.message, 'danger');
} finally {
showLoading(false);
}
}
async function validateAllPlaylists() {
showLoading(true);
try {
const response = await fetch('/api/validate-all-playlists');
if (!response.ok) throw new Error('Failed to validate all playlists');
const results = await response.json();
displayAllPlaylistsResults(results);
} catch (error) {
console.error('Error validating all playlists:', error);
showAlert('Error validating all playlists: ' + error.message, 'danger');
} finally {
showLoading(false);
}
}
function displayValidationResults(results) {
// Update stats
document.getElementById('totalSongs').textContent = results.total_songs;
document.getElementById('exactMatches').textContent = results.summary.exact_match_count;
document.getElementById('fuzzyMatches').textContent = results.summary.fuzzy_match_count;
document.getElementById('missingSongs').textContent = results.summary.missing_count;
// Update tab badges
document.getElementById('exactCount').textContent = results.summary.exact_match_count;
document.getElementById('fuzzyCount').textContent = results.summary.fuzzy_match_count;
document.getElementById('missingCount').textContent = results.summary.missing_count;
// Update playlist title
document.getElementById('currentPlaylistTitle').textContent = results.playlist_title;
// Display results in tabs
displayExactMatches(results.exact_matches);
displayFuzzyMatches(results.fuzzy_matches);
displayMissingSongs(results.missing_songs);
// Show results section
document.getElementById('validationResults').style.display = 'block';
document.getElementById('welcomeState').style.display = 'none';
}
function displayExactMatches(exactMatches) {
const container = document.getElementById('exactMatchesList');
container.innerHTML = '';
if (exactMatches.length === 0) {
container.innerHTML = '<p class="text-muted">No exact matches found.</p>';
return;
}
exactMatches.forEach(match => {
const songItem = createSongItem(match, 'exact');
container.appendChild(songItem);
});
}
function displayFuzzyMatches(fuzzyMatches) {
const container = document.getElementById('fuzzyMatchesList');
container.innerHTML = '';
if (fuzzyMatches.length === 0) {
container.innerHTML = '<p class="text-muted">No fuzzy matches found.</p>';
return;
}
fuzzyMatches.forEach(match => {
const songItem = createSongItem(match, 'fuzzy');
container.appendChild(songItem);
});
}
function displayMissingSongs(missingSongs) {
const container = document.getElementById('missingSongsList');
container.innerHTML = '';
if (missingSongs.length === 0) {
container.innerHTML = '<p class="text-muted">No missing songs found.</p>';
return;
}
missingSongs.forEach(song => {
const songItem = createSongItem(song, 'missing');
container.appendChild(songItem);
});
}
function createSongItem(match, type) {
const songItem = document.createElement('div');
songItem.className = `card mb-2 song-item ${type}-match`;
if (type === 'exact') {
songItem.innerHTML = `
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-1">${match.playlist_artist} - ${match.playlist_title}</h6>
<small class="text-muted">Position: ${match.position}</small>
<div class="mt-2">
<span class="badge bg-success">Exact Match</span>
<span class="badge bg-secondary">${getFileType(match.found_song.path)}</span>
${match.found_song.path.includes('favorites') ? '<span class="badge bg-warning">Favorites</span>' : ''}
${match.found_song.path.includes('history') ? '<span class="badge bg-info">History</span>' : ''}
</div>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-sm btn-outline-primary" onclick="previewVideo('${match.found_song.path.replace(/\\/g, '\\\\')}')">
<i class="fas fa-play"></i> Preview
</button>
</div>
</div>
</div>
`;
} else if (type === 'fuzzy') {
const similarityPercent = Math.round(match.similarity * 100);
// Check if this song is already in pending changes
const isQueued = pendingChanges.some(change =>
change.playlistIndex === currentPlaylistIndex &&
change.songPosition === match.position
);
let updateButton;
if (isQueued) {
updateButton = `
<button class="btn btn-sm btn-secondary" disabled title="Already queued for update">
<i class="fas fa-clock"></i> Queued
</button>
`;
} else {
updateButton = `
<button class="btn btn-sm btn-success" onclick="showUpdateModal(${match.position}, '${match.found_song.artist}', '${match.found_song.title}')">
<i class="fas fa-save"></i> Update
</button>
`;
}
songItem.innerHTML = `
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-1">${match.playlist_artist} - ${match.playlist_title}</h6>
<small class="text-muted">Position: ${match.position}</small>
<div class="mt-2">
<span class="badge bg-warning">Fuzzy Match (${similarityPercent}%)</span>
<span class="badge bg-secondary">${getFileType(match.found_song.path)}</span>
${match.found_song.path.includes('favorites') ? '<span class="badge bg-warning">Favorites</span>' : ''}
${match.found_song.path.includes('history') ? '<span class="badge bg-info">History</span>' : ''}
</div>
<div class="similarity-bar mt-2">
<div class="similarity-fill" style="width: ${similarityPercent}%"></div>
</div>
<div class="mt-2">
<small class="text-muted">Found: ${match.found_song.artist} - ${match.found_song.title}</small>
</div>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-sm btn-outline-primary me-1" onclick="previewVideo('${match.found_song.path.replace(/\\/g, '\\\\')}')">
<i class="fas fa-play"></i> Preview
</button>
${updateButton}
</div>
</div>
</div>
`;
} else if (type === 'missing') {
songItem.innerHTML = `
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-1">${match.artist} - ${match.title}</h6>
<small class="text-muted">Position: ${match.position}</small>
<div class="mt-2">
<span class="badge bg-danger">Missing</span>
<small class="text-muted">${match.reason}</small>
</div>
</div>
</div>
</div>
`;
}
return songItem;
}
function getFileType(path) {
const ext = path.split('.').pop().toLowerCase();
if (ext === 'mp4') return 'MP4';
if (ext === 'mp3') return 'MP3';
if (ext === 'cdg') return 'CDG';
return 'Unknown';
}
function previewVideo(filePath) {
const videoPlayer = document.getElementById('videoPlayer');
const videoUrl = `/api/video/${encodeURIComponent(filePath)}`;
videoPlayer.src = videoUrl;
const modal = new bootstrap.Modal(document.getElementById('videoModal'));
modal.show();
}
function showUpdateModal(songPosition, foundArtist, foundTitle) {
document.getElementById('updatePlaylistIndex').value = currentPlaylistIndex;
document.getElementById('updateSongPosition').value = songPosition;
document.getElementById('currentPlaylistSong').textContent =
currentValidationResults.fuzzy_matches.find(m => m.position === songPosition)?.playlist_artist +
' - ' +
currentValidationResults.fuzzy_matches.find(m => m.position === songPosition)?.playlist_title;
document.getElementById('foundSongInfo').textContent = foundArtist + ' - ' + foundTitle;
document.getElementById('newArtist').value = foundArtist;
document.getElementById('newTitle').value = foundTitle;
const modal = new bootstrap.Modal(document.getElementById('updateSongModal'));
modal.show();
}
async function updatePlaylistSong() {
const playlistIndex = document.getElementById('updatePlaylistIndex').value;
const songPosition = document.getElementById('updateSongPosition').value;
const newArtist = document.getElementById('newArtist').value;
const newTitle = document.getElementById('newTitle').value;
const dryRun = document.getElementById('dryRunCheck').checked;
if (!newArtist || !newTitle) {
showAlert('Please fill in both artist and title fields.', 'warning');
return;
}
// Close the modal immediately to unblock the UI
const modal = bootstrap.Modal.getInstance(document.getElementById('updateSongModal'));
modal.hide();
if (dryRun) {
// For dry run, just show a preview message
showAlert(`Preview: Would update playlist ${parseInt(playlistIndex) + 1} song ${songPosition} to "${newArtist} - ${newTitle}"`, 'info');
} else {
// Check if this song is already in the pending changes
const isDuplicate = pendingChanges.some(change =>
change.playlistIndex === parseInt(playlistIndex) &&
change.songPosition === parseInt(songPosition)
);
if (isDuplicate) {
showAlert(`Song ${songPosition} is already queued for update. Remove it from the batch panel first.`, 'warning');
return;
}
// Add to pending changes queue
pendingChanges.push({
playlistIndex: parseInt(playlistIndex),
songPosition: parseInt(songPosition),
newArtist: newArtist,
newTitle: newTitle,
dryRun: dryRun
});
displayPendingChanges();
// Refresh the current validation results to update button states
if (currentValidationResults) {
displayValidationResults(currentValidationResults);
}
showAlert(`Queued: ${newArtist} - ${newTitle} for batch update`, 'success');
}
}
function updateSongInUI(playlistIndex, songPosition, newArtist, newTitle) {
// Find and update the song item in the current validation results
if (currentValidationResults) {
// Update fuzzy matches
const fuzzyMatch = currentValidationResults.fuzzy_matches.find(
match => match.position === parseInt(songPosition)
);
if (fuzzyMatch) {
// Move from fuzzy matches to exact matches
fuzzyMatch.playlist_artist = newArtist;
fuzzyMatch.playlist_title = newTitle;
fuzzyMatch.match_type = 'exact';
fuzzyMatch.needs_manual_review = false;
// Remove from fuzzy matches and add to exact matches
currentValidationResults.fuzzy_matches = currentValidationResults.fuzzy_matches.filter(
match => match.position !== parseInt(songPosition)
);
currentValidationResults.exact_matches.push(fuzzyMatch);
// Update summary counts
currentValidationResults.summary.fuzzy_match_count--;
currentValidationResults.summary.exact_match_count++;
currentValidationResults.summary.needs_manual_review--;
// Refresh the display
displayValidationResults(currentValidationResults);
}
}
}
function displayAllPlaylistsResults(results) {
// Create a summary view for all playlists
const container = document.getElementById('validationResults');
container.innerHTML = `
<div class="card">
<div class="card-header">
<h5>All Playlists Validation Results</h5>
</div>
<div class="card-body">
<div class="row text-center mb-4">
<div class="col-md-3">
<h4 class="text-primary">${results.total_playlists}</h4>
<small>Total Playlists</small>
</div>
<div class="col-md-3">
<h4 class="text-success">${results.overall_summary.exact_matches}</h4>
<small>Exact Matches</small>
</div>
<div class="col-md-3">
<h4 class="text-warning">${results.overall_summary.fuzzy_matches}</h4>
<small>Fuzzy Matches</small>
</div>
<div class="col-md-3">
<h4 class="text-danger">${results.overall_summary.missing_songs}</h4>
<small>Missing Songs</small>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Playlist</th>
<th>Total Songs</th>
<th>Exact</th>
<th>Fuzzy</th>
<th>Missing</th>
<th>Action</th>
</tr>
</thead>
<tbody>
${results.playlist_results.map((playlist, index) => `
<tr>
<td>${playlist.playlist_title}</td>
<td>${playlist.total_songs}</td>
<td><span class="badge bg-success">${playlist.summary.exact_match_count}</span></td>
<td><span class="badge bg-warning">${playlist.summary.fuzzy_match_count}</span></td>
<td><span class="badge bg-danger">${playlist.summary.missing_count}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="validatePlaylist(${index})">
<i class="fas fa-search"></i> Details
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
container.style.display = 'block';
document.getElementById('welcomeState').style.display = 'none';
}
function showLoading(show) {
document.getElementById('loadingState').style.display = show ? 'block' : 'none';
document.getElementById('validationResults').style.display = show ? 'none' : 'block';
document.getElementById('welcomeState').style.display = show ? 'none' : 'block';
}
function showAlert(message, type) {
// Create alert element
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
function showBatchUpdatePanel(show) {
const panel = document.getElementById('batchUpdatePanel');
panel.classList.toggle('show', show);
}
function displayPendingChanges() {
const container = document.getElementById('queuedChangesList');
const countElement = document.getElementById('pendingCount');
container.innerHTML = '';
if (pendingChanges.length === 0) {
container.innerHTML = '<p class="text-muted p-3">No pending changes.</p>';
countElement.textContent = '0';
showBatchUpdatePanel(false);
return;
}
// Show the batch update panel when there are changes
showBatchUpdatePanel(true);
countElement.textContent = pendingChanges.length;
pendingChanges.forEach((change, index) => {
const changeItem = document.createElement('div');
changeItem.className = `queued-item queued-song`;
// Find the original song info
const originalSong = currentValidationResults?.fuzzy_matches.find(m => m.position === change.songPosition);
const originalArtist = originalSong?.playlist_artist || 'Unknown';
const originalTitle = originalSong?.playlist_title || 'Unknown';
changeItem.innerHTML = `
<div class="change-info">
<strong>Playlist ${change.playlistIndex + 1}</strong> - Song ${change.songPosition}
<br>
<small class="text-muted">From: ${originalArtist} - ${originalTitle}</small>
<br>
<small class="text-success">To: ${change.newArtist} - ${change.newTitle}</small>
</div>
<button class="btn btn-sm btn-danger remove-btn" onclick="removePendingChange(${index})" title="Remove from queue">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(changeItem);
});
}
function removePendingChange(index) {
pendingChanges.splice(index, 1);
displayPendingChanges();
// Refresh the current validation results to update button states
if (currentValidationResults) {
displayValidationResults(currentValidationResults);
}
}
async function applyAllPendingChanges() {
if (pendingChanges.length === 0) {
showAlert('No pending changes to apply.', 'info');
return;
}
showLoading(true);
showBatchUpdatePanel(false); // Hide panel while applying
try {
const response = await fetch('/api/apply-all-updates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
changes: pendingChanges
})
});
if (!response.ok) throw new Error('Failed to apply all updates');
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
// Refresh playlists to show updated results
loadPlaylists();
// Clear pending changes
pendingChanges = [];
displayPendingChanges();
} else {
showAlert(result.error || 'Failed to apply all updates', 'danger');
}
} catch (error) {
console.error('Error applying all updates:', error);
showAlert('Error applying all updates: ' + error.message, 'danger');
} finally {
showLoading(false);
showBatchUpdatePanel(true); // Show panel after applying
}
}
</script>
</body>
</html>