402 lines
16 KiB
HTML
402 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Remaining Songs After Cleanup - Karaoke Library</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>
|
|
.song-card {
|
|
border-left: 4px solid #28a745;
|
|
margin-bottom: 1rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.song-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.file-type-badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
.channel-badge {
|
|
font-size: 0.8rem;
|
|
}
|
|
.stats-card {
|
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
|
color: white;
|
|
}
|
|
.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;
|
|
}
|
|
.back-button {
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid">
|
|
<!-- Header -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<a href="/" class="btn btn-outline-secondary back-button">
|
|
<i class="fas fa-arrow-left"></i> Back to Duplicates
|
|
</a>
|
|
<h1 class="d-inline-block ms-3">Remaining Songs After Cleanup</h1>
|
|
</div>
|
|
<div class="text-end">
|
|
<small class="text-muted">All songs that remain after duplicate removal</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h3 id="totalSongs">-</h3>
|
|
<p class="mb-0">Total Songs</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h3 id="mp4Count">-</h3>
|
|
<p class="mb-0">MP4 Videos</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h3 id="mp3Count">-</h3>
|
|
<p class="mb-0">MP3/CDG Files</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stats-card">
|
|
<div class="card-body text-center">
|
|
<h3 id="uniqueArtists">-</h3>
|
|
<p class="mb-0">Unique Artists</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filter-section">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<label for="searchInput" class="form-label">Search</label>
|
|
<input type="text" class="form-control" id="searchInput" placeholder="Search by artist or title...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="fileTypeFilter" class="form-label">File Type</label>
|
|
<select class="form-select" id="fileTypeFilter">
|
|
<option value="all">All Types</option>
|
|
<option value="mp4">MP4 Videos</option>
|
|
<option value="mp3">MP3/CDG Files</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="artistFilter" class="form-label">Artist Filter</label>
|
|
<input type="text" class="form-control" id="artistFilter" placeholder="Filter by artist...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="perPageSelect" class="form-label">Per Page</label>
|
|
<select class="form-select" id="perPageSelect">
|
|
<option value="25">25</option>
|
|
<option value="50" selected>50</option>
|
|
<option value="100">100</option>
|
|
<option value="200">200</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Songs List -->
|
|
<div id="songsContainer">
|
|
<div class="loading">
|
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
|
<p class="mt-2">Loading remaining songs...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<nav aria-label="Songs pagination">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="pagination-info">
|
|
Showing <span id="showingInfo">-</span> of <span id="totalInfo">-</span> songs
|
|
</div>
|
|
<ul class="pagination mb-0" id="pagination">
|
|
<!-- Pagination will be generated here -->
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Modal -->
|
|
<div class="modal fade" id="videoModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="videoModalTitle">Video Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<video id="videoPlayer" class="w-100" controls>
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</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 currentFilters = {
|
|
search: '',
|
|
file_type: 'all',
|
|
artist: '',
|
|
per_page: 50
|
|
};
|
|
|
|
// Initialize the page
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadSongs();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Search input
|
|
const searchInput = document.getElementById('searchInput');
|
|
let searchTimeout;
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
currentFilters.search = this.value;
|
|
currentPage = 1;
|
|
loadSongs();
|
|
}, 300);
|
|
});
|
|
|
|
// File type filter
|
|
document.getElementById('fileTypeFilter').addEventListener('change', function() {
|
|
currentFilters.file_type = this.value;
|
|
currentPage = 1;
|
|
loadSongs();
|
|
});
|
|
|
|
// Artist filter
|
|
const artistFilter = document.getElementById('artistFilter');
|
|
let artistTimeout;
|
|
artistFilter.addEventListener('input', function() {
|
|
clearTimeout(artistTimeout);
|
|
artistTimeout = setTimeout(() => {
|
|
currentFilters.artist = this.value;
|
|
currentPage = 1;
|
|
loadSongs();
|
|
}, 300);
|
|
});
|
|
|
|
// Per page selector
|
|
document.getElementById('perPageSelect').addEventListener('change', function() {
|
|
currentFilters.per_page = parseInt(this.value);
|
|
currentPage = 1;
|
|
loadSongs();
|
|
});
|
|
}
|
|
|
|
async function loadSongs() {
|
|
const container = document.getElementById('songsContainer');
|
|
container.innerHTML = '<div class="loading"><i class="fas fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading songs...</p></div>';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: currentPage,
|
|
per_page: currentFilters.per_page,
|
|
search: currentFilters.search,
|
|
file_type: currentFilters.file_type,
|
|
artist: currentFilters.artist
|
|
});
|
|
|
|
const response = await fetch(`/api/remaining-songs?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
displaySongs(data.songs, data.pagination);
|
|
updateStats(data.songs, data.pagination);
|
|
} else {
|
|
container.innerHTML = `<div class="alert alert-danger">Error: ${data.error}</div>`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading songs:', error);
|
|
container.innerHTML = '<div class="alert alert-danger">Error loading songs. Please try again.</div>';
|
|
}
|
|
}
|
|
|
|
function displaySongs(songs, pagination) {
|
|
const container = document.getElementById('songsContainer');
|
|
|
|
if (songs.length === 0) {
|
|
container.innerHTML = '<div class="alert alert-info">No songs found matching your filters.</div>';
|
|
return;
|
|
}
|
|
|
|
const songsHtml = songs.map(song => `
|
|
<div class="card song-card">
|
|
<div class="card-body">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h5 class="card-title mb-1">${escapeHtml(song.title)}</h5>
|
|
<p class="card-text text-muted mb-2">${escapeHtml(song.artist)}</p>
|
|
<div class="path-text">${escapeHtml(song.path)}</div>
|
|
</div>
|
|
<div class="col-md-4 text-end">
|
|
<span class="badge bg-primary file-type-badge me-2">${song.file_type}</span>
|
|
${song.channel ? `<span class="badge bg-info channel-badge me-2">${escapeHtml(song.channel)}</span>` : ''}
|
|
${song.file_type === 'MP4' ?
|
|
`<button class="btn btn-sm btn-outline-primary" onclick="openVideoPlayer('${escapeHtml(song.path)}', '${escapeHtml(song.artist)}', '${escapeHtml(song.title)}')">
|
|
<i class="fas fa-play"></i> Play
|
|
</button>` :
|
|
''
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
container.innerHTML = songsHtml;
|
|
updatePagination(pagination);
|
|
}
|
|
|
|
function updateStats(songs, pagination) {
|
|
// Update total songs
|
|
document.getElementById('totalSongs').textContent = pagination.total_songs;
|
|
|
|
// Count file types
|
|
const mp4Count = songs.filter(song => song.file_type === 'MP4').length;
|
|
const mp3Count = songs.filter(song => song.file_type === 'MP3').length;
|
|
|
|
// Get unique artists from current page (this is a simplified count)
|
|
const uniqueArtists = new Set(songs.map(song => song.artist)).size;
|
|
|
|
document.getElementById('mp4Count').textContent = mp4Count;
|
|
document.getElementById('mp3Count').textContent = mp3Count;
|
|
document.getElementById('uniqueArtists').textContent = uniqueArtists;
|
|
}
|
|
|
|
function updatePagination(pagination) {
|
|
const paginationContainer = document.getElementById('pagination');
|
|
const showingInfo = document.getElementById('showingInfo');
|
|
const totalInfo = document.getElementById('totalInfo');
|
|
|
|
// Update info text
|
|
const start = (pagination.current_page - 1) * pagination.per_page + 1;
|
|
const end = Math.min(start + pagination.per_page - 1, pagination.total_songs);
|
|
showingInfo.textContent = `${start}-${end}`;
|
|
totalInfo.textContent = pagination.total_songs;
|
|
|
|
// Generate pagination buttons
|
|
let paginationHtml = '';
|
|
|
|
// Previous button
|
|
paginationHtml += `
|
|
<li class="page-item ${pagination.current_page === 1 ? 'disabled' : ''}">
|
|
<a class="page-link" href="#" onclick="changePage(${pagination.current_page - 1})">Previous</a>
|
|
</li>
|
|
`;
|
|
|
|
// Page numbers
|
|
const startPage = Math.max(1, pagination.current_page - 2);
|
|
const endPage = Math.min(pagination.total_pages, pagination.current_page + 2);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
paginationHtml += `
|
|
<li class="page-item ${i === pagination.current_page ? 'active' : ''}">
|
|
<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
|
|
</li>
|
|
`;
|
|
}
|
|
|
|
// Next button
|
|
paginationHtml += `
|
|
<li class="page-item ${pagination.current_page === pagination.total_pages ? 'disabled' : ''}">
|
|
<a class="page-link" href="#" onclick="changePage(${pagination.current_page + 1})">Next</a>
|
|
</li>
|
|
`;
|
|
|
|
paginationContainer.innerHTML = paginationHtml;
|
|
}
|
|
|
|
function changePage(page) {
|
|
currentPage = page;
|
|
loadSongs();
|
|
}
|
|
|
|
function openVideoPlayer(filePath, artist, title) {
|
|
const modal = new bootstrap.Modal(document.getElementById('videoModal'));
|
|
const videoPlayer = document.getElementById('videoPlayer');
|
|
const modalTitle = document.getElementById('videoModalTitle');
|
|
|
|
modalTitle.textContent = `${artist} - ${title}`;
|
|
|
|
// Normalize the path for video playback
|
|
const normalizedPath = normalizePath(filePath);
|
|
const encodedPath = encodeURIComponent(normalizedPath);
|
|
const videoUrl = `/api/video/${encodedPath}`;
|
|
|
|
videoPlayer.src = videoUrl;
|
|
modal.show();
|
|
|
|
// Handle video errors
|
|
videoPlayer.onerror = function() {
|
|
console.error('Error loading video:', videoUrl);
|
|
alert(`Error loading video: ${filePath}`);
|
|
};
|
|
}
|
|
|
|
function normalizePath(filePath) {
|
|
// Simple path normalization - just handle :// corruption
|
|
if (filePath.includes('://')) {
|
|
return filePath.replace('://', ':\\');
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |