1610 lines
69 KiB
HTML
1610 lines
69 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">
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
<script>
|
|
// Fallback for Sortable.js if CDN fails
|
|
if (typeof Sortable === 'undefined') {
|
|
console.warn('Primary Sortable.js CDN failed, trying fallback...');
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js';
|
|
script.onload = function() {
|
|
console.log('Sortable.js loaded from fallback CDN');
|
|
};
|
|
script.onerror = function() {
|
|
console.error('Both Sortable.js CDNs failed to load');
|
|
alert('Warning: Sortable.js library failed to load. Drag and drop functionality will not work.');
|
|
};
|
|
document.head.appendChild(script);
|
|
}
|
|
</script>
|
|
<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;
|
|
}
|
|
|
|
/* Drag and Drop Styles */
|
|
.sortable-list {
|
|
min-height: 50px;
|
|
}
|
|
.sortable-item {
|
|
cursor: grab;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.sortable-item:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.sortable-item:active {
|
|
cursor: grabbing;
|
|
}
|
|
.sortable-ghost {
|
|
opacity: 0.5;
|
|
background-color: #e9ecef;
|
|
}
|
|
.sortable-chosen {
|
|
background-color: #fff3cd;
|
|
border: 2px dashed #ffc107;
|
|
}
|
|
.priority-indicator {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: #007bff;
|
|
color: white;
|
|
border-radius: 50%;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
.priority-1 { background: #28a745; }
|
|
.priority-2 { background: #17a2b8; }
|
|
.priority-3 { background: #ffc107; color: #212529; }
|
|
.priority-4 { background: #fd7e14; }
|
|
.priority-5 { background: #dc3545; }
|
|
|
|
.drag-handle {
|
|
cursor: grab;
|
|
color: #6c757d;
|
|
margin-right: 8px;
|
|
padding: 4px;
|
|
border-radius: 3px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.drag-handle:hover {
|
|
color: #495057;
|
|
background-color: #e9ecef;
|
|
}
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
background-color: #dee2e6;
|
|
}
|
|
|
|
.priority-info {
|
|
background-color: #e7f3ff;
|
|
border: 1px solid #b3d9ff;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
margin-bottom: 10px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Video Player Modal Styles */
|
|
.video-modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1050;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.video-modal-content {
|
|
position: relative;
|
|
margin: 2% auto;
|
|
padding: 0;
|
|
width: 90%;
|
|
max-width: 800px;
|
|
background-color: #000;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.video-modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px 20px;
|
|
background-color: #343a40;
|
|
color: white;
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
|
|
.video-modal-title {
|
|
margin: 0;
|
|
font-size: 1.1rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.video-modal-close {
|
|
color: #aaa;
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
line-height: 1;
|
|
}
|
|
|
|
.video-modal-close:hover {
|
|
color: white;
|
|
}
|
|
|
|
.video-container {
|
|
position: relative;
|
|
width: 100%;
|
|
background-color: #000;
|
|
}
|
|
|
|
.video-player {
|
|
width: 100%;
|
|
height: auto;
|
|
max-height: 70vh;
|
|
display: block;
|
|
}
|
|
|
|
.play-button {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
margin-left: 8px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.play-button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
.play-button:disabled {
|
|
background-color: #6c757d;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.file-path-display {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
margin-top: 4px;
|
|
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>
|
|
<small class="text-muted" id="drag-status">
|
|
<i class="fas fa-spinner fa-spin"></i> Loading drag-and-drop...
|
|
</small>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-md-6">
|
|
<button class="btn btn-primary w-100" onclick="savePriorityPreferences()" id="save-priority-btn" disabled>
|
|
<i class="fas fa-sort"></i> Save Priority Preferences
|
|
</button>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<button class="btn btn-info w-100" onclick="resetPriorityPreferences()">
|
|
<i class="fas fa-undo"></i> Reset Priorities
|
|
</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 = [];
|
|
let priorityChanges = {};
|
|
let sortableInstances = [];
|
|
|
|
// Load data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Check if Sortable.js is loaded
|
|
if (typeof Sortable === 'undefined') {
|
|
console.error('Sortable.js library not loaded!');
|
|
alert('Error: Sortable.js library not loaded. Drag and drop functionality will not work.');
|
|
} else {
|
|
console.log('Sortable.js library loaded successfully');
|
|
}
|
|
|
|
loadStats();
|
|
loadArtists();
|
|
loadPriorityPreferences();
|
|
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.replace(/'/g, "\\'") + '\')"]');
|
|
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);
|
|
}
|
|
}
|
|
|
|
async function loadPriorityPreferences() {
|
|
try {
|
|
const response = await fetch('/api/load-priority-preferences');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
priorityChanges = data.preferences;
|
|
updatePrioritySaveButton();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading priority preferences:', 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.replace(/"/g, '\\"') + '"]');
|
|
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);
|
|
}
|
|
|
|
// Initialize sortable for all duplicate groups
|
|
setTimeout(() => {
|
|
console.log('Initializing sortable instances...');
|
|
updateDragStatus('loading', 'Initializing drag-and-drop...');
|
|
initializeSortable();
|
|
console.log('Sortable initialization complete');
|
|
}, 100);
|
|
}
|
|
|
|
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, '_');
|
|
|
|
// Get current priority order for this song
|
|
const songKey = duplicate.artist + ' - ' + duplicate.title;
|
|
const currentPriorities = priorityChanges[songKey] || [];
|
|
|
|
// Create all versions array (kept + skipped)
|
|
const allVersions = [
|
|
{
|
|
path: duplicate.kept_version,
|
|
file_type: getFileType(duplicate.kept_version),
|
|
channel: extractChannel(duplicate.kept_version),
|
|
is_kept: true
|
|
},
|
|
...duplicate.skipped_versions.map(v => ({...v, is_kept: false}))
|
|
];
|
|
|
|
// Apply current priority order if it exists
|
|
if (currentPriorities.length > 0) {
|
|
allVersions.sort((a, b) => {
|
|
const aIndex = currentPriorities.indexOf(a.path);
|
|
const bIndex = currentPriorities.indexOf(b.path);
|
|
if (aIndex === -1 && bIndex === -1) return 0;
|
|
if (aIndex === -1) return 1;
|
|
if (bIndex === -1) return 0;
|
|
return aIndex - bIndex;
|
|
});
|
|
}
|
|
|
|
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.replace(/'/g, "\\'") + '\')">' +
|
|
'<i class="fas fa-chevron-down"></i> Details' +
|
|
'</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="card-body" id="details-' + safeId + '" style="display: none;">' +
|
|
'<div class="priority-info">' +
|
|
'<i class="fas fa-info-circle"></i> ' +
|
|
'<strong>Drag and drop to reorder file priorities.</strong> ' +
|
|
'The top file will be kept, others will be skipped. ' +
|
|
'Click "Save Priority Preferences" to apply these changes for future CLI runs.' +
|
|
'<br><small class="text-muted"><i class="fas fa-hand-pointer"></i> Use the grip handle (⋮⋮) to drag items</small>' +
|
|
'</div>' +
|
|
'<!-- Sortable Versions List -->' +
|
|
'<h6><i class="fas fa-sort"></i> FILE PRIORITIES (Drag to reorder):</h6>' +
|
|
'<div class="sortable-list" id="sortable-' + safeId + '">' +
|
|
allVersions.map((version, index) =>
|
|
'<div class="card mb-2 sortable-item ' + (version.is_kept ? 'kept-version' : 'skipped-version') + '" ' +
|
|
'data-path="' + version.path + '" data-index="' + index + '">' +
|
|
'<div class="priority-indicator priority-' + Math.min(index + 1, 5) + '">' + (index + 1) + '</div>' +
|
|
'<div class="card-body">' +
|
|
'<div class="d-flex align-items-start">' +
|
|
'<div class="drag-handle">' +
|
|
'<i class="fas fa-grip-vertical"></i>' +
|
|
'</div>' +
|
|
'<div class="flex-grow-1">' +
|
|
'<div class="path-text">' + version.path + '</div>' +
|
|
'<span class="badge ' + (version.is_kept ? 'bg-success' : 'bg-danger') + ' file-type-badge">' + version.file_type + '</span>' +
|
|
'<span class="badge ' + (version.is_kept ? 'bg-info' : 'bg-warning') + ' channel-badge">' + version.channel + '</span>' +
|
|
(version.is_kept ? '<span class="badge bg-success ms-1 status-badge">KEPT</span>' : '<span class="badge bg-danger ms-1 status-badge">SKIPPED</span>') +
|
|
(version.file_type === 'MP4' ?
|
|
'<button class="play-button" onclick="openVideoPlayer(\'' + version.path.replace(/'/g, "\\'") + '\', \'' + duplicate.artist.replace(/'/g, "\\'") + '\', \'' + duplicate.title.replace(/'/g, "\\'") + '\')">' +
|
|
'<i class="fas fa-play"></i> Play' +
|
|
'</button>' :
|
|
''
|
|
) +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>'
|
|
).join('') +
|
|
'</div>' +
|
|
'</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;
|
|
}
|
|
}
|
|
|
|
// Priority Management Functions
|
|
function initializeSortable() {
|
|
// Destroy existing instances
|
|
sortableInstances.forEach(instance => {
|
|
if (instance && typeof instance.destroy === 'function') {
|
|
instance.destroy();
|
|
}
|
|
});
|
|
sortableInstances = [];
|
|
|
|
// Check if Sortable.js is available
|
|
if (typeof Sortable === 'undefined') {
|
|
console.error('Sortable.js not available');
|
|
updateDragStatus('error', 'Drag-and-drop not available');
|
|
return;
|
|
}
|
|
|
|
let successCount = 0;
|
|
let totalLists = document.querySelectorAll('.sortable-list').length;
|
|
|
|
// Initialize new sortable instances
|
|
document.querySelectorAll('.sortable-list').forEach(list => {
|
|
try {
|
|
console.log('Initializing sortable for list:', list.id);
|
|
const sortable = Sortable.create(list, {
|
|
handle: '.drag-handle',
|
|
animation: 150,
|
|
ghostClass: 'sortable-ghost',
|
|
chosenClass: 'sortable-chosen',
|
|
onStart: function(evt) {
|
|
console.log('Sortable started:', evt);
|
|
},
|
|
onEnd: function(evt) {
|
|
console.log('Sortable ended:', evt);
|
|
const songKey = getSongKeyFromSortableList(evt.to);
|
|
updatePriorityOrder(songKey, evt.to);
|
|
updatePriorityIndicators(evt.to);
|
|
}
|
|
});
|
|
sortableInstances.push(sortable);
|
|
successCount++;
|
|
console.log('Sortable instance created successfully');
|
|
} catch (error) {
|
|
console.error('Error creating sortable instance:', error);
|
|
}
|
|
});
|
|
|
|
// Update status
|
|
if (successCount > 0) {
|
|
updateDragStatus('success', `Drag-and-drop ready (${successCount} lists)`);
|
|
} else {
|
|
updateDragStatus('error', 'No drag-and-drop lists found');
|
|
}
|
|
}
|
|
|
|
function updateDragStatus(status, message) {
|
|
const statusElement = document.getElementById('drag-status');
|
|
if (statusElement) {
|
|
let icon = '';
|
|
let color = '';
|
|
|
|
switch (status) {
|
|
case 'success':
|
|
icon = 'fas fa-check';
|
|
color = 'text-success';
|
|
break;
|
|
case 'error':
|
|
icon = 'fas fa-exclamation-triangle';
|
|
color = 'text-danger';
|
|
break;
|
|
case 'loading':
|
|
icon = 'fas fa-spinner fa-spin';
|
|
color = 'text-muted';
|
|
break;
|
|
}
|
|
|
|
statusElement.innerHTML = `<i class="${icon}"></i> ${message}`;
|
|
statusElement.className = `text-muted ${color}`;
|
|
}
|
|
}
|
|
|
|
function getSongKeyFromSortableList(listElement) {
|
|
try {
|
|
const detailsElement = listElement.closest('.card-body');
|
|
const cardElement = detailsElement.closest('.duplicate-card');
|
|
const titleElement = cardElement.querySelector('h6 strong');
|
|
const songKey = titleElement.textContent;
|
|
console.log('Extracted song key:', songKey);
|
|
return songKey;
|
|
} catch (error) {
|
|
console.error('Error extracting song key:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function updatePriorityOrder(songKey, listElement) {
|
|
try {
|
|
const items = Array.from(listElement.querySelectorAll('.sortable-item'));
|
|
const newOrder = items.map(item => item.getAttribute('data-path'));
|
|
|
|
console.log('Updating priority order for:', songKey);
|
|
console.log('New order:', newOrder);
|
|
|
|
priorityChanges[songKey] = newOrder;
|
|
updatePrioritySaveButton();
|
|
|
|
// Update visual indicators after reordering
|
|
updateVisualIndicators(listElement);
|
|
} catch (error) {
|
|
console.error('Error updating priority order:', error);
|
|
}
|
|
}
|
|
|
|
function updateVisualIndicators(listElement) {
|
|
try {
|
|
const items = Array.from(listElement.querySelectorAll('.sortable-item'));
|
|
|
|
items.forEach((item, index) => {
|
|
// Update the CSS class for the card
|
|
item.classList.remove('kept-version', 'skipped-version');
|
|
if (index === 0) {
|
|
item.classList.add('kept-version');
|
|
} else {
|
|
item.classList.add('skipped-version');
|
|
}
|
|
|
|
// Update the file type badge color
|
|
const fileTypeBadge = item.querySelector('.file-type-badge');
|
|
if (fileTypeBadge) {
|
|
fileTypeBadge.classList.remove('bg-success', 'bg-danger');
|
|
fileTypeBadge.classList.add(index === 0 ? 'bg-success' : 'bg-danger');
|
|
}
|
|
|
|
// Update the channel badge color
|
|
const channelBadge = item.querySelector('.channel-badge');
|
|
if (channelBadge) {
|
|
channelBadge.classList.remove('bg-info', 'bg-warning');
|
|
channelBadge.classList.add(index === 0 ? 'bg-info' : 'bg-warning');
|
|
}
|
|
|
|
// Update the KEPT/SKIPPED badge
|
|
const statusBadge = item.querySelector('.status-badge');
|
|
if (statusBadge) {
|
|
statusBadge.classList.remove('bg-success', 'bg-danger');
|
|
statusBadge.classList.add(index === 0 ? 'bg-success' : 'bg-danger');
|
|
statusBadge.textContent = index === 0 ? 'KEPT' : 'SKIPPED';
|
|
}
|
|
});
|
|
|
|
console.log('Visual indicators updated for song list');
|
|
} catch (error) {
|
|
console.error('Error updating visual indicators:', error);
|
|
}
|
|
}
|
|
|
|
function updatePriorityIndicators(listElement) {
|
|
const items = Array.from(listElement.querySelectorAll('.sortable-item'));
|
|
items.forEach((item, index) => {
|
|
const indicator = item.querySelector('.priority-indicator');
|
|
if (indicator) {
|
|
indicator.className = 'priority-indicator priority-' + Math.min(index + 1, 5);
|
|
indicator.textContent = index + 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
function updatePrioritySaveButton() {
|
|
const saveBtn = document.getElementById('save-priority-btn');
|
|
const hasChanges = Object.keys(priorityChanges).length > 0;
|
|
|
|
if (hasChanges) {
|
|
saveBtn.disabled = false;
|
|
saveBtn.textContent = 'Save Priority Preferences (' + Object.keys(priorityChanges).length + ' songs)';
|
|
} else {
|
|
saveBtn.disabled = true;
|
|
saveBtn.textContent = 'Save Priority Preferences';
|
|
}
|
|
}
|
|
|
|
async function savePriorityPreferences() {
|
|
if (Object.keys(priorityChanges).length === 0) {
|
|
alert('No priority changes to save');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/save-priority-preferences', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
priority_changes: priorityChanges
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert('✅ Priority preferences saved successfully!\n\n' + result.message);
|
|
priorityChanges = {};
|
|
updatePrioritySaveButton();
|
|
} else {
|
|
alert('❌ Error: ' + result.error);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error saving priority preferences:', error);
|
|
alert('❌ Error saving priority preferences');
|
|
}
|
|
}
|
|
|
|
async function resetPriorityPreferences() {
|
|
if (confirm('Are you sure you want to reset all priority preferences? This will restore the default priority order.')) {
|
|
try {
|
|
const response = await fetch('/api/reset-priority-preferences', {
|
|
method: 'POST'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
alert('✅ Priority preferences reset successfully!');
|
|
priorityChanges = {};
|
|
updatePrioritySaveButton();
|
|
loadDuplicates(); // Refresh the display
|
|
} else {
|
|
alert('❌ Error: ' + result.error);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error resetting priority preferences:', error);
|
|
alert('❌ Error resetting priority preferences');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Video Player Functions
|
|
function normalizePath(filePath) {
|
|
// Fix malformed paths that have been corrupted with ://
|
|
// Example: z://MP4KaraFun KaraokeKaraoke I m Not In Love - 10CC.mp4
|
|
// Should be: z:\MP4\KaraFun Karaoke\Karaoke I'm Not In Love - 10CC.mp4
|
|
|
|
if (filePath.includes('://')) {
|
|
console.log('DEBUG: Detected malformed path, attempting to fix:', filePath);
|
|
|
|
// Extract drive letter and rest of path
|
|
const match = filePath.match(/^([a-zA-Z]):\/\/(.+)$/);
|
|
if (match) {
|
|
const driveLetter = match[1];
|
|
let restOfPath = match[2];
|
|
|
|
// Try to reconstruct the proper path structure
|
|
// Look for common patterns in the corrupted path
|
|
|
|
// Pattern 1: Handle the specific case where "Karaoke" appears twice
|
|
// Example: "MP4KaraFun KaraokeKaraoke I m Not In Love - 10CC.mp4"
|
|
// Should become: "MP4\KaraFun Karaoke\Karaoke I'm Not In Love - 10CC.mp4"
|
|
const karaokeDoubleMatch = restOfPath.match(/^MP4([A-Za-z\s]+)KaraokeKaraoke(.+)$/);
|
|
if (karaokeDoubleMatch) {
|
|
const channelName = karaokeDoubleMatch[1] + "Karaoke";
|
|
const fileName = "Karaoke" + karaokeDoubleMatch[2];
|
|
const fixedPath = driveLetter + ":\\MP4\\" + channelName + "\\" + fileName;
|
|
console.log('DEBUG: Fixed path (pattern 1 - double karaoke):', fixedPath);
|
|
return fixedPath;
|
|
}
|
|
|
|
// Pattern 2: MP4 followed by channel name (e.g., MP4KaraFun Karaoke)
|
|
const mp4Match = restOfPath.match(/^MP4([A-Za-z\s]+Karaoke)(.+)$/);
|
|
if (mp4Match) {
|
|
const channelName = mp4Match[1];
|
|
const fileName = mp4Match[2];
|
|
const fixedPath = driveLetter + ":\\MP4\\" + channelName + "\\" + fileName;
|
|
console.log('DEBUG: Fixed path (pattern 2):', fixedPath);
|
|
return fixedPath;
|
|
}
|
|
|
|
// Pattern 3: Direct channel name followed by filename
|
|
const channelMatch = restOfPath.match(/^([A-Za-z\s]+Karaoke)(.+)$/);
|
|
if (channelMatch) {
|
|
const channelName = channelMatch[1];
|
|
const fileName = channelMatch[2];
|
|
const fixedPath = driveLetter + ":\\MP4\\" + channelName + "\\" + fileName;
|
|
console.log('DEBUG: Fixed path (pattern 3):', fixedPath);
|
|
return fixedPath;
|
|
}
|
|
|
|
// Pattern 4: Look for any known channel names
|
|
const knownChannels = ['Sing King Karaoke', 'KaraFun Karaoke', 'Stingray Karaoke'];
|
|
for (const channel of knownChannels) {
|
|
if (channel.toLowerCase().replace(/\s/g, '') === restOfPath.toLowerCase().replace(/\s/g, '').substring(0, channel.toLowerCase().replace(/\s/g, '').length)) {
|
|
// Extract the part before and after the channel name
|
|
const channelLower = channel.toLowerCase().replace(/\s/g, '');
|
|
const restLower = restOfPath.toLowerCase().replace(/\s/g, '');
|
|
const channelIndex = restLower.indexOf(channelLower);
|
|
|
|
if (channelIndex >= 0) {
|
|
// Reconstruct the path
|
|
const beforeChannel = restOfPath.substring(0, channelIndex);
|
|
const afterChannel = restOfPath.substring(channelIndex + channel.length);
|
|
|
|
// If there's content before the channel, it might be a folder like "MP4"
|
|
let fixedPath;
|
|
if (beforeChannel && beforeChannel.toLowerCase() === 'mp4') {
|
|
fixedPath = driveLetter + ":\\" + beforeChannel + "\\" + channel + afterChannel;
|
|
} else {
|
|
fixedPath = driveLetter + ":\\MP4\\" + channel + afterChannel;
|
|
}
|
|
|
|
console.log('DEBUG: Fixed path (pattern 4):', fixedPath);
|
|
return fixedPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pattern 5: Try to split by common separators and reconstruct
|
|
// Look for patterns like "MP4KaraFunKaraoke" -> "MP4\KaraFun Karaoke"
|
|
if (restOfPath.toLowerCase().includes('karaoke')) {
|
|
// Try to find where "karaoke" appears and reconstruct
|
|
const karaokeIndex = restOfPath.toLowerCase().indexOf('karaoke');
|
|
if (karaokeIndex > 0) {
|
|
const beforeKaraoke = restOfPath.substring(0, karaokeIndex);
|
|
const afterKaraoke = restOfPath.substring(karaokeIndex + 7); // length of "karaoke"
|
|
|
|
// If beforeKaraoke starts with "MP4", extract the channel name
|
|
if (beforeKaraoke.toLowerCase().startsWith('mp4')) {
|
|
const channelPart = beforeKaraoke.substring(3); // Remove "MP4"
|
|
if (channelPart) {
|
|
const fixedPath = driveLetter + ":\\MP4\\" + channelPart + " Karaoke" + afterKaraoke;
|
|
console.log('DEBUG: Fixed path (pattern 5):', fixedPath);
|
|
return fixedPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: just replace :// with :\ and hope for the best
|
|
const fallbackPath = filePath.replace('://', ':\\');
|
|
console.log('DEBUG: Fallback path fix:', fallbackPath);
|
|
return fallbackPath;
|
|
}
|
|
}
|
|
|
|
return filePath;
|
|
}
|
|
|
|
function openVideoPlayer(filePath, artist, title) {
|
|
const modal = document.getElementById('videoModal');
|
|
const videoPlayer = document.getElementById('videoPlayer');
|
|
const modalTitle = document.getElementById('videoModalTitle');
|
|
|
|
// Set modal title
|
|
modalTitle.textContent = artist + ' - ' + title;
|
|
|
|
// Normalize the file path to fix any malformed paths
|
|
const normalizedPath = normalizePath(filePath);
|
|
|
|
// Encode the file path for the URL
|
|
const encodedPath = encodeURIComponent(normalizedPath);
|
|
|
|
console.log('DEBUG: Opening video player for:', filePath);
|
|
console.log('DEBUG: Normalized path:', normalizedPath);
|
|
console.log('DEBUG: Encoded path:', encodedPath);
|
|
console.log('DEBUG: Video URL:', '/api/video/' + encodedPath);
|
|
|
|
// Clear any previous source
|
|
videoPlayer.src = '';
|
|
|
|
// Set video source using Flask route
|
|
videoPlayer.src = '/api/video/' + encodedPath;
|
|
|
|
// Show modal
|
|
modal.style.display = 'block';
|
|
|
|
// Add event listener for video load
|
|
videoPlayer.onloadeddata = function() {
|
|
console.log('Video loaded successfully');
|
|
console.log('Video duration:', videoPlayer.duration);
|
|
console.log('Video ready state:', videoPlayer.readyState);
|
|
};
|
|
|
|
// Add error handling
|
|
videoPlayer.onerror = function(e) {
|
|
console.error('Error loading video:', filePath);
|
|
console.error('Normalized path:', normalizedPath);
|
|
console.error('Video error details:', e);
|
|
console.error('Video error code:', videoPlayer.error ? videoPlayer.error.code : 'unknown');
|
|
console.error('Video error message:', videoPlayer.error ? videoPlayer.error.message : 'unknown');
|
|
|
|
// Show more specific error messages
|
|
let errorMessage = 'Error loading video. ';
|
|
if (videoPlayer.error) {
|
|
switch(videoPlayer.error.code) {
|
|
case 1:
|
|
errorMessage += 'The video loading was aborted.';
|
|
break;
|
|
case 2:
|
|
errorMessage += 'Network error occurred while loading the video.';
|
|
break;
|
|
case 3:
|
|
errorMessage += 'The video decoding failed. The file may be corrupted or in an unsupported format.';
|
|
break;
|
|
case 4:
|
|
errorMessage += 'The video format is not supported by your browser.';
|
|
break;
|
|
default:
|
|
errorMessage += 'Unknown error occurred.';
|
|
}
|
|
}
|
|
|
|
alert(errorMessage + '\n\nCheck console for details.');
|
|
};
|
|
|
|
// Add more detailed event listeners
|
|
videoPlayer.onloadstart = function() {
|
|
console.log('Video load started');
|
|
};
|
|
|
|
videoPlayer.onprogress = function() {
|
|
console.log('Video loading progress');
|
|
};
|
|
|
|
videoPlayer.oncanplay = function() {
|
|
console.log('Video can start playing');
|
|
console.log('Video dimensions:', videoPlayer.videoWidth, 'x', videoPlayer.videoHeight);
|
|
};
|
|
|
|
videoPlayer.oncanplaythrough = function() {
|
|
console.log('Video can play through without buffering');
|
|
};
|
|
|
|
videoPlayer.onabort = function() {
|
|
console.log('Video loading aborted');
|
|
};
|
|
|
|
videoPlayer.onstalled = function() {
|
|
console.log('Video loading stalled');
|
|
};
|
|
|
|
videoPlayer.onsuspend = function() {
|
|
console.log('Video loading suspended');
|
|
};
|
|
|
|
// Try to play the video automatically
|
|
videoPlayer.onloadedmetadata = function() {
|
|
console.log('Video metadata loaded');
|
|
console.log('Video duration:', videoPlayer.duration);
|
|
console.log('Video dimensions:', videoPlayer.videoWidth, 'x', videoPlayer.videoHeight);
|
|
|
|
// Try to play the video
|
|
videoPlayer.play().then(function() {
|
|
console.log('Video started playing automatically');
|
|
}).catch(function(error) {
|
|
console.log('Auto-play failed (this is normal):', error);
|
|
console.log('User will need to click play manually');
|
|
});
|
|
};
|
|
}
|
|
|
|
function closeVideoPlayer() {
|
|
try {
|
|
const modal = document.getElementById('videoModal');
|
|
const videoPlayer = document.getElementById('videoPlayer');
|
|
|
|
if (videoPlayer) {
|
|
// Pause video
|
|
videoPlayer.pause();
|
|
|
|
// Clear source and remove all event listeners
|
|
videoPlayer.src = '';
|
|
videoPlayer.onloadeddata = null;
|
|
videoPlayer.onerror = null;
|
|
videoPlayer.onloadstart = null;
|
|
videoPlayer.onprogress = null;
|
|
videoPlayer.oncanplay = null;
|
|
videoPlayer.oncanplaythrough = null;
|
|
videoPlayer.onabort = null;
|
|
videoPlayer.onstalled = null;
|
|
videoPlayer.onsuspend = null;
|
|
videoPlayer.onloadedmetadata = null;
|
|
}
|
|
|
|
if (modal) {
|
|
// Hide modal
|
|
modal.style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error closing video player:', error);
|
|
}
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
window.onclick = function(event) {
|
|
try {
|
|
const modal = document.getElementById('videoModal');
|
|
if (event.target === modal) {
|
|
closeVideoPlayer();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in window click handler:', error);
|
|
}
|
|
}
|
|
|
|
// Close modal with Escape key
|
|
document.addEventListener('keydown', function(event) {
|
|
try {
|
|
if (event.key === 'Escape') {
|
|
closeVideoPlayer();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in keydown handler:', error);
|
|
}
|
|
});
|
|
|
|
// Debug function to test drag and drop
|
|
function testDragAndDrop() {
|
|
console.log('Testing drag and drop functionality...');
|
|
console.log('Sortable.js loaded:', typeof Sortable !== 'undefined');
|
|
console.log('Sortable instances:', sortableInstances.length);
|
|
console.log('Sortable lists found:', document.querySelectorAll('.sortable-list').length);
|
|
|
|
// Test if we can find any sortable items
|
|
const items = document.querySelectorAll('.sortable-item');
|
|
console.log('Sortable items found:', items.length);
|
|
|
|
if (items.length > 0) {
|
|
console.log('First sortable item:', items[0]);
|
|
console.log('Drag handle found:', items[0].querySelector('.drag-handle'));
|
|
}
|
|
|
|
// Try to reinitialize sortable
|
|
console.log('Reinitializing sortable...');
|
|
initializeSortable();
|
|
}
|
|
|
|
// Make test function available globally
|
|
window.testDragAndDrop = testDragAndDrop;
|
|
</script>
|
|
|
|
<!-- Video Player Modal -->
|
|
<div id="videoModal" class="video-modal">
|
|
<div class="video-modal-content">
|
|
<div class="video-modal-header">
|
|
<h5 class="video-modal-title" id="videoModalTitle">Video Player</h5>
|
|
<button class="video-modal-close" onclick="closeVideoPlayer()">×</button>
|
|
</div>
|
|
<div class="video-container">
|
|
<video id="videoPlayer" class="video-player" controls>
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html> |