1002 lines
44 KiB
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> |