798 lines
35 KiB
HTML
798 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Karaoke Duplicate Review - Web UI</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
.duplicate-card {
|
|
border-left: 4px solid #dc3545;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.kept-version {
|
|
background-color: #d4edda;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
.skipped-version {
|
|
background-color: #f8d7da;
|
|
border-left: 4px solid #dc3545;
|
|
}
|
|
.file-type-badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
.channel-badge {
|
|
font-size: 0.8rem;
|
|
}
|
|
.stats-card {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
.file-type-card {
|
|
transition: transform 0.2s;
|
|
}
|
|
.file-type-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
.metric-highlight {
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
}
|
|
.metric-warning {
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
}
|
|
.filter-section {
|
|
background-color: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
.pagination-info {
|
|
font-size: 0.9rem;
|
|
color: #6c757d;
|
|
}
|
|
.path-text {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.85rem;
|
|
word-break: break-all;
|
|
}
|
|
</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-music"></i> Karaoke Duplicate Review</h1>
|
|
<p class="mb-0">Interactive interface for reviewing and understanding your duplicate songs</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Dashboard -->
|
|
<div class="row mb-4" id="stats-section">
|
|
<!-- Current Totals -->
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="total-songs">-</h4>
|
|
<p class="mb-0">Total Songs</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="total-duplicates">-</h4>
|
|
<p class="mb-0">Songs with Duplicates</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="total-files">-</h4>
|
|
<p class="mb-0">Files to Skip</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="total-remaining">-</h4>
|
|
<p class="mb-0">Files After Cleanup</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="space-savings">-</h4>
|
|
<p class="mb-0">Space Savings</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h4 id="avg-duplicates">-</h4>
|
|
<p class="mb-0">Avg Duplicates</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Type Breakdown -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="card file-type-card">
|
|
<div class="card-header bg-primary text-white">
|
|
<h6 class="mb-0"><i class="fas fa-list"></i> Current File Types</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6 text-center">
|
|
<h5 id="total-mp4">-</h5>
|
|
<small class="text-muted">MP4</small>
|
|
</div>
|
|
<div class="col-6 text-center">
|
|
<h5 id="total-mp3">-</h5>
|
|
<small class="text-muted">MP3</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card file-type-card">
|
|
<div class="card-header bg-danger text-white">
|
|
<h6 class="mb-0"><i class="fas fa-trash"></i> Files to Skip</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6 text-center">
|
|
<h5 id="skip-mp4">-</h5>
|
|
<small class="text-muted">MP4</small>
|
|
</div>
|
|
<div class="col-6 text-center">
|
|
<h5 id="skip-mp3">-</h5>
|
|
<small class="text-muted">MP3</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card file-type-card">
|
|
<div class="card-header bg-success text-white">
|
|
<h6 class="mb-0"><i class="fas fa-check"></i> After Cleanup</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6 text-center">
|
|
<h5 id="remaining-mp4">-</h5>
|
|
<small class="text-muted">MP4</small>
|
|
</div>
|
|
<div class="col-6 text-center">
|
|
<h5 id="remaining-mp3">-</h5>
|
|
<small class="text-muted">MP3</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Options -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="filter-section">
|
|
<h5><i class="fas fa-eye"></i> View Options</h5>
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<label for="view-mode" class="form-label">View Mode</label>
|
|
<select class="form-select" id="view-mode" onchange="changeViewMode()">
|
|
<option value="all">All Songs</option>
|
|
<option value="artists">Group by Artist</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="sort-by" class="form-label">Sort By</label>
|
|
<select class="form-select" id="sort-by" onchange="applyFilters()">
|
|
<option value="artist">Artist</option>
|
|
<option value="title">Title</option>
|
|
<option value="duplicates">Most Duplicates</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="artist-select" class="form-label">Quick Artist Select</label>
|
|
<select class="form-select" id="artist-select" onchange="selectArtist()">
|
|
<option value="">All Artists</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label"> </label>
|
|
<button class="btn btn-success w-100" onclick="saveChanges()" id="save-btn" disabled>
|
|
<i class="fas fa-save"></i> Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<div class="filter-section">
|
|
<h5><i class="fas fa-filter"></i> Filters</h5>
|
|
<div class="row">
|
|
<div class="col-md-2">
|
|
<label for="artist-filter" class="form-label">Artist</label>
|
|
<input type="text" class="form-control" id="artist-filter" placeholder="Filter by artist...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="title-filter" class="form-label">Title</label>
|
|
<input type="text" class="form-control" id="title-filter" placeholder="Filter by title...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="channel-filter" class="form-label">Channel</label>
|
|
<select class="form-select" id="channel-filter">
|
|
<option value="">All Channels</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="file-type-filter" class="form-label">File Type</label>
|
|
<select class="form-select" id="file-type-filter">
|
|
<option value="">All Types</option>
|
|
<option value="mp4">MP4</option>
|
|
<option value="mp3">MP3</option>
|
|
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="min-duplicates" class="form-label">Min Duplicates</label>
|
|
<input type="number" class="form-control" id="min-duplicates" min="0" value="0">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label"> </label>
|
|
<button class="btn btn-primary w-100" onclick="applyFilters()">
|
|
<i class="fas fa-search"></i> Apply Filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-md-3">
|
|
<button class="btn btn-success w-100" onclick="downloadMp3Songs()">
|
|
<i class="fas fa-download"></i> Download MP3 Song List
|
|
</button>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<small class="text-muted">
|
|
<i class="fas fa-info-circle"></i>
|
|
Download a JSON file containing all MP3 songs that remain after cleanup,
|
|
sorted by artist and title. Use this list to find replacement videos.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Duplicates List -->
|
|
<div class="row">
|
|
<div class="col">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-list"></i> Duplicate Songs</h5>
|
|
<div class="pagination-info" id="pagination-info">
|
|
Showing 0 of 0 results
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="loading" class="loading">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p>Loading duplicates...</p>
|
|
</div>
|
|
<div id="duplicates-container"></div>
|
|
|
|
<!-- Pagination -->
|
|
<nav aria-label="Duplicates pagination" class="mt-4">
|
|
<ul class="pagination justify-content-center" id="pagination">
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
let currentPage = 1;
|
|
let totalPages = 1;
|
|
let currentFilters = {};
|
|
let viewMode = 'all';
|
|
let pendingChanges = [];
|
|
let allArtists = [];
|
|
|
|
// Load data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadStats();
|
|
loadArtists();
|
|
loadDuplicates();
|
|
});
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const response = await fetch('/api/stats');
|
|
const data = await response.json();
|
|
|
|
// Main statistics
|
|
document.getElementById('total-songs').textContent = data.total_songs.toLocaleString();
|
|
document.getElementById('total-duplicates').textContent = data.total_duplicates.toLocaleString();
|
|
document.getElementById('total-files').textContent = data.total_files_to_skip.toLocaleString();
|
|
document.getElementById('total-remaining').textContent = data.total_remaining.toLocaleString();
|
|
document.getElementById('avg-duplicates').textContent = (data.total_files_to_skip / data.total_duplicates).toFixed(1);
|
|
|
|
// Calculate space savings percentage
|
|
const savingsPercent = ((data.total_files_to_skip / data.total_songs) * 100).toFixed(1);
|
|
document.getElementById('space-savings').textContent = `${savingsPercent}%`;
|
|
|
|
// Current file types
|
|
document.getElementById('total-mp4').textContent = data.total_file_types.MP4.toLocaleString();
|
|
document.getElementById('total-mp3').textContent = data.total_file_types.MP3.toLocaleString();
|
|
|
|
// Files to skip
|
|
document.getElementById('skip-mp4').textContent = data.skip_file_types.MP4.toLocaleString();
|
|
document.getElementById('skip-mp3').textContent = data.skip_file_types.MP3.toLocaleString();
|
|
|
|
// Files after cleanup
|
|
document.getElementById('remaining-mp4').textContent = data.remaining_file_types.MP4.toLocaleString();
|
|
document.getElementById('remaining-mp3').textContent = data.remaining_file_types.MP3.toLocaleString();
|
|
|
|
// Populate channel filter
|
|
const channelSelect = document.getElementById('channel-filter');
|
|
channelSelect.innerHTML = '<option value="">All Channels</option>';
|
|
Object.keys(data.channels).forEach(channel => {
|
|
const option = document.createElement('option');
|
|
option.value = channel.toLowerCase();
|
|
option.textContent = `${channel} (${data.channels[channel]})`;
|
|
channelSelect.appendChild(option);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
}
|
|
}
|
|
|
|
async function loadDuplicates(page = 1) {
|
|
const loading = document.getElementById('loading');
|
|
const container = document.getElementById('duplicates-container');
|
|
|
|
loading.style.display = 'block';
|
|
container.innerHTML = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: page,
|
|
per_page: 20,
|
|
...currentFilters
|
|
});
|
|
|
|
const response = await fetch(`/api/duplicates?${params}`);
|
|
const data = await response.json();
|
|
|
|
currentPage = data.page;
|
|
totalPages = data.total_pages;
|
|
|
|
displayDuplicates(data.duplicates);
|
|
updatePagination(data.total, data.page, data.per_page, data.total_pages);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading duplicates:', error);
|
|
container.innerHTML = '<div class="alert alert-danger">Error loading duplicates</div>';
|
|
} finally {
|
|
loading.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function toggleDetails(songKey) {
|
|
const details = document.getElementById(`details-${songKey}`);
|
|
if (!details) {
|
|
console.error('Details element not found for:', songKey);
|
|
return;
|
|
}
|
|
|
|
// Find the button that was clicked
|
|
const button = document.querySelector(`[onclick="toggleDetails('${songKey}')"]`);
|
|
if (!button) {
|
|
console.error('Button not found for:', songKey);
|
|
return;
|
|
}
|
|
|
|
const icon = button.querySelector('i');
|
|
if (!icon) {
|
|
console.error('Icon not found for:', songKey);
|
|
return;
|
|
}
|
|
|
|
if (details.style.display === 'none' || details.style.display === '') {
|
|
details.style.display = 'block';
|
|
icon.className = 'fas fa-chevron-up';
|
|
} else {
|
|
details.style.display = 'none';
|
|
icon.className = 'fas fa-chevron-down';
|
|
}
|
|
}
|
|
|
|
function updatePagination(total, page, perPage, totalPages) {
|
|
const info = document.getElementById('pagination-info');
|
|
const start = (page - 1) * perPage + 1;
|
|
const end = Math.min(page * perPage, total);
|
|
info.textContent = `Showing ${start}-${end} of ${total.toLocaleString()} results`;
|
|
|
|
const pagination = document.getElementById('pagination');
|
|
pagination.innerHTML = '';
|
|
|
|
// Previous button
|
|
const prevLi = document.createElement('li');
|
|
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
|
|
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${page - 1})">Previous</a>`;
|
|
pagination.appendChild(prevLi);
|
|
|
|
// Page numbers
|
|
const startPage = Math.max(1, page - 2);
|
|
const endPage = Math.min(totalPages, page + 2);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const li = document.createElement('li');
|
|
li.className = `page-item ${i === page ? 'active' : ''}`;
|
|
li.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${i})">${i}</a>`;
|
|
pagination.appendChild(li);
|
|
}
|
|
|
|
// Next button
|
|
const nextLi = document.createElement('li');
|
|
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
|
|
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${page + 1})">Next</a>`;
|
|
pagination.appendChild(nextLi);
|
|
}
|
|
|
|
function applyFilters() {
|
|
currentFilters = {
|
|
artist: document.getElementById('artist-filter').value,
|
|
title: document.getElementById('title-filter').value,
|
|
channel: document.getElementById('channel-filter').value,
|
|
file_type: document.getElementById('file-type-filter').value,
|
|
min_duplicates: document.getElementById('min-duplicates').value
|
|
};
|
|
|
|
loadDuplicates(1);
|
|
}
|
|
|
|
function getFileType(path) {
|
|
const lower = path.toLowerCase();
|
|
if (lower.endsWith('.mp4')) return 'MP4';
|
|
if (lower.endsWith('.mp3')) return 'MP3';
|
|
if (lower.endsWith('.cdg')) return 'MP3'; // Treat CDG as MP3 since they're paired
|
|
return 'Unknown';
|
|
}
|
|
|
|
function extractChannel(path) {
|
|
const lower = path.toLowerCase();
|
|
const parts = path.split('\\');
|
|
|
|
// Look for specific known channels first
|
|
const knownChannels = ['Sing King Karaoke', 'KaraFun Karaoke', 'Stingray Karaoke'];
|
|
for (const channel of knownChannels) {
|
|
if (lower.includes(channel.toLowerCase())) {
|
|
return channel;
|
|
}
|
|
}
|
|
|
|
// Look for MP4 folder structure: MP4/ChannelName/song.mp4
|
|
for (let i = 0; i < parts.length; i++) {
|
|
if (parts[i].toLowerCase() === 'mp4' && i < parts.length - 1) {
|
|
// If MP4 is found, return the next folder (the actual channel)
|
|
if (i + 1 < parts.length) {
|
|
const nextPart = parts[i + 1];
|
|
// Skip if the next part is the filename (no extension means it's a folder)
|
|
if (nextPart.indexOf('.') === -1) {
|
|
return nextPart;
|
|
} else {
|
|
return 'MP4 Root'; // File is directly in MP4 folder
|
|
}
|
|
} else {
|
|
return 'MP4 Root';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for any folder that contains 'karaoke' (fallback)
|
|
for (const part of parts) {
|
|
if (part.toLowerCase().includes('karaoke')) {
|
|
return part;
|
|
}
|
|
}
|
|
|
|
// If no specific channel found, return the folder containing the file
|
|
if (parts.length >= 2) {
|
|
const parentFolder = parts[parts.length - 2]; // Second to last part (folder containing the file)
|
|
// If parent folder is MP4, then file is in root
|
|
if (parentFolder.toLowerCase() === 'mp4') {
|
|
return 'MP4 Root';
|
|
}
|
|
return parentFolder;
|
|
}
|
|
|
|
return 'Unknown';
|
|
}
|
|
|
|
async function loadArtists() {
|
|
try {
|
|
const response = await fetch('/api/artists');
|
|
const data = await response.json();
|
|
|
|
allArtists = data.artists;
|
|
|
|
// Populate artist select dropdown
|
|
const artistSelect = document.getElementById('artist-select');
|
|
artistSelect.innerHTML = '<option value="">All Artists</option>';
|
|
allArtists.forEach(artist => {
|
|
const option = document.createElement('option');
|
|
option.value = artist.name;
|
|
option.textContent = `${artist.name} (${artist.total_duplicates} duplicates)`;
|
|
artistSelect.appendChild(option);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading artists:', error);
|
|
}
|
|
}
|
|
|
|
function changeViewMode() {
|
|
viewMode = document.getElementById('view-mode').value;
|
|
loadDuplicates(1);
|
|
}
|
|
|
|
function selectArtist() {
|
|
const selectedArtist = document.getElementById('artist-select').value;
|
|
if (selectedArtist) {
|
|
document.getElementById('artist-filter').value = selectedArtist;
|
|
applyFilters();
|
|
}
|
|
}
|
|
|
|
function toggleKeepFile(songKey, filePath, artist, title, keptVersion) {
|
|
const change = {
|
|
type: 'keep_file',
|
|
song_key: songKey,
|
|
file_path: filePath,
|
|
artist: artist,
|
|
title: title,
|
|
kept_version: keptVersion
|
|
};
|
|
|
|
pendingChanges.push(change);
|
|
updateSaveButton();
|
|
|
|
// Visual feedback
|
|
const element = document.querySelector(`[data-path="${filePath}"]`);
|
|
if (element) {
|
|
element.style.opacity = '0.5';
|
|
element.style.backgroundColor = '#d4edda';
|
|
}
|
|
}
|
|
|
|
function updateSaveButton() {
|
|
const saveBtn = document.getElementById('save-btn');
|
|
if (pendingChanges.length > 0) {
|
|
saveBtn.disabled = false;
|
|
saveBtn.textContent = `Save Changes (${pendingChanges.length})`;
|
|
} else {
|
|
saveBtn.disabled = true;
|
|
saveBtn.textContent = 'Save Changes';
|
|
}
|
|
}
|
|
|
|
async function saveChanges() {
|
|
if (pendingChanges.length === 0) {
|
|
alert('No changes to save');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/save-changes', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
changes: pendingChanges
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert(`✅ ${result.message}`);
|
|
pendingChanges = [];
|
|
updateSaveButton();
|
|
loadDuplicates(); // Refresh the data
|
|
} else {
|
|
alert(`❌ Error: ${result.error}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error saving changes:', error);
|
|
alert('❌ Error saving changes');
|
|
}
|
|
}
|
|
|
|
function displayDuplicates(duplicates) {
|
|
const container = document.getElementById('duplicates-container');
|
|
|
|
if (duplicates.length === 0) {
|
|
container.innerHTML = '<div class="alert alert-info">No duplicates found matching your filters.</div>';
|
|
return;
|
|
}
|
|
|
|
if (viewMode === 'artists') {
|
|
displayArtistsView(duplicates);
|
|
} else {
|
|
displayAllSongsView(duplicates);
|
|
}
|
|
}
|
|
|
|
function displayArtistsView(duplicates) {
|
|
const container = document.getElementById('duplicates-container');
|
|
|
|
// Group by artist
|
|
const artists = {};
|
|
duplicates.forEach(duplicate => {
|
|
const artist = duplicate.artist;
|
|
if (!artists[artist]) {
|
|
artists[artist] = {
|
|
name: artist,
|
|
songs: [],
|
|
totalDuplicates: 0
|
|
};
|
|
}
|
|
artists[artist].songs.push(duplicate);
|
|
artists[artist].totalDuplicates += duplicate.total_duplicates;
|
|
});
|
|
|
|
// Sort artists alphabetically
|
|
const sortedArtists = Object.values(artists).sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
container.innerHTML = sortedArtists.map(artist => `
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-user"></i> ${artist.name}
|
|
<span class="badge bg-light text-dark ms-2">${artist.songs.length} songs, ${artist.totalDuplicates} duplicates</span>
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
${artist.songs.map(duplicate => createSongCard(duplicate)).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function displayAllSongsView(duplicates) {
|
|
const container = document.getElementById('duplicates-container');
|
|
container.innerHTML = duplicates.map(duplicate => createSongCard(duplicate)).join('');
|
|
}
|
|
|
|
function createSongCard(duplicate) {
|
|
// Create a safe ID by replacing special characters
|
|
const safeId = `${duplicate.artist} - ${duplicate.title}`.replace(/[^a-zA-Z0-9\s\-]/g, '_');
|
|
|
|
return `
|
|
<div class="card duplicate-card">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">
|
|
<strong>${duplicate.artist} - ${duplicate.title}</strong>
|
|
<span class="badge bg-primary ms-2">${duplicate.total_duplicates} duplicates</span>
|
|
</h6>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-secondary me-2" onclick="toggleDetails('${safeId}')">
|
|
<i class="fas fa-chevron-down"></i> Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body" id="details-${safeId}" style="display: none;">
|
|
<!-- Kept Version -->
|
|
<div class="row mb-3">
|
|
<div class="col">
|
|
<h6 class="text-success"><i class="fas fa-check-circle"></i> KEPT VERSION:</h6>
|
|
<div class="card kept-version">
|
|
<div class="card-body">
|
|
<div class="path-text">${duplicate.kept_version}</div>
|
|
<span class="badge bg-success file-type-badge">${getFileType(duplicate.kept_version)}</span>
|
|
<span class="badge bg-info channel-badge">${extractChannel(duplicate.kept_version)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skipped Versions -->
|
|
<h6 class="text-danger"><i class="fas fa-times-circle"></i> SKIPPED VERSIONS (${duplicate.skipped_versions.length}):</h6>
|
|
${duplicate.skipped_versions.map(version => `
|
|
<div class="card skipped-version mb-2" data-path="${version.path}">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<div class="path-text">${version.path}</div>
|
|
<span class="badge bg-danger file-type-badge">${version.file_type}</span>
|
|
<span class="badge bg-warning channel-badge">${version.channel}</span>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-success ms-2"
|
|
onclick="toggleKeepFile('${safeId}', '${version.path}', '${duplicate.artist}', '${duplicate.title}', '${duplicate.kept_version}')"
|
|
title="Keep this file instead">
|
|
<i class="fas fa-check"></i> Keep
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function downloadMp3Songs() {
|
|
try {
|
|
// Show loading state
|
|
const button = event.target.closest('button');
|
|
const originalText = button.innerHTML;
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
|
|
button.disabled = true;
|
|
|
|
// Trigger download
|
|
const response = await fetch('/api/download/mp3-songs');
|
|
|
|
if (response.ok) {
|
|
// Create a blob from the response
|
|
const blob = await response.blob();
|
|
|
|
// Create download link
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'mp3SongList.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
// Show success message
|
|
alert('MP3 Song List downloaded successfully!');
|
|
} else {
|
|
const errorData = await response.json();
|
|
alert('Error downloading MP3 song list: ' + (errorData.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error downloading MP3 songs:', error);
|
|
alert('Error downloading MP3 song list: ' + error.message);
|
|
} finally {
|
|
// Restore button state
|
|
const button = event.target.closest('button');
|
|
button.innerHTML = originalText;
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |