Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>

This commit is contained in:
mbrucedogs 2025-07-26 17:49:32 -05:00
parent 16745f5270
commit 54bce66584
2 changed files with 645 additions and 148 deletions

View File

@ -158,6 +158,102 @@ def extract_channel(path: str) -> str:
return 'Unknown'
def normalize_path(file_path: str) -> str:
"""Normalize malformed file paths that have been corrupted with ://."""
# 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 '://' in file_path:
print(f"DEBUG: Detected malformed path, attempting to fix: {file_path}")
# Extract drive letter and rest of path
import re
match = re.match(r'^([a-zA-Z])://(.+)$', file_path)
if match:
drive_letter = match.group(1)
rest_of_path = match.group(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"
karaoke_double_match = re.match(r'^MP4([A-Za-z\s]+)KaraokeKaraoke(.+)$', rest_of_path)
if karaoke_double_match:
channel_name = karaoke_double_match.group(1) + "Karaoke"
file_name = "Karaoke" + karaoke_double_match.group(2)
fixed_path = f"{drive_letter}:\\MP4\\{channel_name}\\{file_name}"
print(f"DEBUG: Fixed path (pattern 1 - double karaoke): {fixed_path}")
return fixed_path
# Pattern 2: MP4 followed by channel name (e.g., MP4KaraFun Karaoke)
mp4_match = re.match(r'^MP4([A-Za-z\s]+Karaoke)(.+)$', rest_of_path)
if mp4_match:
channel_name = mp4_match.group(1)
file_name = mp4_match.group(2)
fixed_path = f"{drive_letter}:\\MP4\\{channel_name}\\{file_name}"
print(f"DEBUG: Fixed path (pattern 2): {fixed_path}")
return fixed_path
# Pattern 3: Direct channel name followed by filename
channel_match = re.match(r'^([A-Za-z\s]+Karaoke)(.+)$', rest_of_path)
if channel_match:
channel_name = channel_match.group(1)
file_name = channel_match.group(2)
fixed_path = f"{drive_letter}:\\MP4\\{channel_name}\\{file_name}"
print(f"DEBUG: Fixed path (pattern 3): {fixed_path}")
return fixed_path
# Pattern 4: Look for any known channel names
known_channels = ['Sing King Karaoke', 'KaraFun Karaoke', 'Stingray Karaoke']
for channel in known_channels:
if channel.lower().replace(' ', '') in rest_of_path.lower().replace(' ', ''):
# Extract the part before and after the channel name
channel_lower = channel.lower().replace(' ', '')
rest_lower = rest_of_path.lower().replace(' ', '')
channel_index = rest_lower.find(channel_lower)
if channel_index >= 0:
# Reconstruct the path
before_channel = rest_of_path[:channel_index]
after_channel = rest_of_path[channel_index + len(channel):]
# If there's content before the channel, it might be a folder like "MP4"
if before_channel and before_channel.lower() in ['mp4']:
fixed_path = f"{drive_letter}:\\{before_channel}\\{channel}{after_channel}"
else:
fixed_path = f"{drive_letter}:\\MP4\\{channel}{after_channel}"
print(f"DEBUG: Fixed path (pattern 4): {fixed_path}")
return fixed_path
# Pattern 5: Try to split by common separators and reconstruct
# Look for patterns like "MP4KaraFunKaraoke" -> "MP4\KaraFun Karaoke"
if 'karaoke' in rest_of_path.lower():
# Try to find where "karaoke" appears and reconstruct
karaoke_index = rest_of_path.lower().find('karaoke')
if karaoke_index > 0:
before_karaoke = rest_of_path[:karaoke_index]
after_karaoke = rest_of_path[karaoke_index + 7:] # length of "karaoke"
# If before_karaoke starts with "MP4", extract the channel name
if before_karaoke.lower().startswith('mp4'):
channel_part = before_karaoke[3:] # Remove "MP4"
if channel_part:
fixed_path = f"{drive_letter}:\\MP4\\{channel_part} Karaoke{after_karaoke}"
print(f"DEBUG: Fixed path (pattern 5): {fixed_path}")
return fixed_path
# Fallback: just replace :// with :\ and hope for the best
fallback_path = file_path.replace('://', ':\\')
print(f"DEBUG: Fallback path fix: {fallback_path}")
return fallback_path
return file_path
@app.route('/')
def index():
"""Main dashboard page."""
@ -538,54 +634,83 @@ def serve_video(file_path):
import urllib.parse
decoded_path = urllib.parse.unquote(file_path)
# Normalize the path to fix any malformed paths
normalized_path = normalize_path(decoded_path)
# Debug logging
print(f"DEBUG: Video request for path: {decoded_path}")
print(f"DEBUG: Normalized path: {normalized_path}")
print(f"DEBUG: Current working directory: {os.getcwd()}")
# Security check: ensure the path is within allowed directories
# This prevents directory traversal attacks
if '..' in decoded_path:
if '..' in normalized_path:
print(f"DEBUG: Security check failed - path contains '..'")
return jsonify({'error': 'Invalid file path'}), 400
# On Windows, allow absolute paths with drive letters
# On Unix-like systems, block absolute paths
if os.name == 'nt': # Windows
if decoded_path.startswith('/') and not decoded_path[1:].startswith(':'):
if normalized_path.startswith('/') and not normalized_path[1:].startswith(':'):
print(f"DEBUG: Security check failed - Unix-style absolute path on Windows")
return jsonify({'error': 'Invalid file path'}), 400
else: # Unix-like systems
if decoded_path.startswith('/'):
if normalized_path.startswith('/'):
print(f"DEBUG: Security check failed - absolute path on Unix")
return jsonify({'error': 'Invalid file path'}), 400
# Check if file exists
if not os.path.exists(decoded_path):
print(f"DEBUG: File does not exist: {decoded_path}")
if not os.path.exists(normalized_path):
print(f"DEBUG: File does not exist: {normalized_path}")
return jsonify({'error': 'Video file not found'}), 404
# Check if it's a video file
if not decoded_path.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv')):
print(f"DEBUG: Invalid file type: {decoded_path}")
# Check if it's a video file and determine MIME type
file_extension = os.path.splitext(normalized_path)[1].lower()
mime_types = {
'.mp4': 'video/mp4',
'.avi': 'video/x-msvideo',
'.mkv': 'video/x-matroska',
'.mov': 'video/quicktime',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.webm': 'video/webm'
}
if file_extension not in mime_types:
print(f"DEBUG: Invalid file type: {normalized_path}")
return jsonify({'error': 'Invalid file type'}), 400
mime_type = mime_types[file_extension]
# Get file info for debugging
file_size = os.path.getsize(decoded_path)
file_size = os.path.getsize(normalized_path)
print(f"DEBUG: File exists, size: {file_size} bytes")
print(f"DEBUG: MIME type: {mime_type}")
# Serve the video file
directory = os.path.dirname(decoded_path)
filename = os.path.basename(decoded_path)
directory = os.path.dirname(normalized_path)
filename = os.path.basename(normalized_path)
print(f"DEBUG: Serving from directory: {directory}")
print(f"DEBUG: Filename: {filename}")
return send_from_directory(
# Add headers for better video streaming
response = send_from_directory(
directory,
filename,
mimetype='video/mp4' # Adjust based on file type if needed
mimetype=mime_type
)
# Add CORS headers to allow cross-origin requests
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Range'
# Add cache control headers
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
except Exception as e:
print(f"DEBUG: Exception in serve_video: {str(e)}")
return jsonify({'error': f'Error serving video: {str(e)}'}), 500

View File

@ -7,6 +7,22 @@
<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;
@ -112,9 +128,17 @@
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 {
@ -379,6 +403,9 @@
<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">
@ -496,6 +523,14 @@
// 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();
@ -516,7 +551,7 @@
// Calculate space savings percentage
const savingsPercent = ((data.total_files_to_skip / data.total_songs) * 100).toFixed(1);
document.getElementById('space-savings').textContent = `${savingsPercent}%`;
document.getElementById('space-savings').textContent = savingsPercent + '%';
// Current file types
document.getElementById('total-mp4').textContent = data.total_file_types.MP4.toLocaleString();
@ -536,7 +571,7 @@
Object.keys(data.channels).forEach(channel => {
const option = document.createElement('option');
option.value = channel.toLowerCase();
option.textContent = `${channel} (${data.channels[channel]})`;
option.textContent = channel + ' (' + data.channels[channel] + ')';
channelSelect.appendChild(option);
});
@ -559,7 +594,7 @@
...currentFilters
});
const response = await fetch(`/api/duplicates?${params}`);
const response = await fetch('/api/duplicates?' + params);
const data = await response.json();
currentPage = data.page;
@ -579,14 +614,14 @@
function toggleDetails(songKey) {
const details = document.getElementById(`details-${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}')"]`);
const button = document.querySelector('[onclick="toggleDetails(\'' + songKey.replace(/'/g, "\\'") + '\')"]');
if (!button) {
console.error('Button not found for:', songKey);
return;
@ -611,15 +646,15 @@
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`;
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>`;
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
@ -628,15 +663,15 @@
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>`;
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>`;
nextLi.className = 'page-item ' + (page === totalPages ? 'disabled' : '');
nextLi.innerHTML = '<a class="page-link" href="#" onclick="loadDuplicates(' + (page + 1) + ')">Next</a>';
pagination.appendChild(nextLi);
}
@ -723,7 +758,7 @@
allArtists.forEach(artist => {
const option = document.createElement('option');
option.value = artist.name;
option.textContent = `${artist.name} (${artist.total_duplicates} duplicates)`;
option.textContent = artist.name + ' (' + artist.total_duplicates + ' duplicates)';
artistSelect.appendChild(option);
});
@ -774,7 +809,7 @@
updateSaveButton();
// Visual feedback
const element = document.querySelector(`[data-path="${filePath}"]`);
const element = document.querySelector('[data-path="' + filePath.replace(/"/g, '\\"') + '"]');
if (element) {
element.style.opacity = '0.5';
element.style.backgroundColor = '#d4edda';
@ -785,7 +820,7 @@
const saveBtn = document.getElementById('save-btn');
if (pendingChanges.length > 0) {
saveBtn.disabled = false;
saveBtn.textContent = `Save Changes (${pendingChanges.length})`;
saveBtn.textContent = 'Save Changes (' + pendingChanges.length + ')';
} else {
saveBtn.disabled = true;
saveBtn.textContent = 'Save Changes';
@ -812,12 +847,12 @@
const result = await response.json();
if (result.success) {
alert(`✅ ${result.message}`);
alert('✅ ' + result.message);
pendingChanges = [];
updateSaveButton();
loadDuplicates(); // Refresh the data
} else {
alert(`❌ Error: ${result.error}`);
alert('❌ Error: ' + result.error);
}
} catch (error) {
@ -842,7 +877,10 @@
// 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);
}
@ -867,19 +905,19 @@
// 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('');
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) {
@ -889,10 +927,10 @@
function createSongCard(duplicate) {
// Create a safe ID by replacing special characters
const safeId = `${duplicate.artist} - ${duplicate.title}`.replace(/[^a-zA-Z0-9\s\-]/g, '_');
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 songKey = duplicate.artist + ' - ' + duplicate.title;
const currentPriorities = priorityChanges[songKey] || [];
// Create all versions array (kept + skipped)
@ -918,61 +956,59 @@
});
}
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;">
<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.
</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">KEPT</span>' : '<span class="badge bg-danger ms-1">SKIPPED</span>'}
${version.file_type === 'MP4' ?
`<button class="play-button" onclick="openVideoPlayer('${version.path}', '${duplicate.artist.replace(/'/g, "\\'")}', '${duplicate.title.replace(/'/g, "\\'")}')">
<i class="fas fa-play"></i> Play
</button>` :
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>
`;
) +
'</div>' +
'</div>' +
'</div>' +
'</div>'
).join('') +
'</div>' +
'</div>' +
'</div>';
}
async function downloadMp3Songs() {
@ -1020,39 +1056,156 @@
// Priority Management Functions
function initializeSortable() {
// Destroy existing instances
sortableInstances.forEach(instance => instance.destroy());
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');
return titleElement.textContent;
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) {
@ -1060,7 +1213,7 @@
items.forEach((item, index) => {
const indicator = item.querySelector('.priority-indicator');
if (indicator) {
indicator.className = `priority-indicator priority-${Math.min(index + 1, 5)}`;
indicator.className = 'priority-indicator priority-' + Math.min(index + 1, 5);
indicator.textContent = index + 1;
}
});
@ -1072,7 +1225,7 @@
if (hasChanges) {
saveBtn.disabled = false;
saveBtn.textContent = `Save Priority Preferences (${Object.keys(priorityChanges).length} songs)`;
saveBtn.textContent = 'Save Priority Preferences (' + Object.keys(priorityChanges).length + ' songs)';
} else {
saveBtn.disabled = true;
saveBtn.textContent = 'Save Priority Preferences';
@ -1099,11 +1252,11 @@
const result = await response.json();
if (result.success) {
alert(`✅ Priority preferences saved successfully!\n\n${result.message}`);
alert('✅ Priority preferences saved successfully!\n\n' + result.message);
priorityChanges = {};
updatePrioritySaveButton();
} else {
alert(`❌ Error: ${result.error}`);
alert('❌ Error: ' + result.error);
}
} catch (error) {
@ -1127,7 +1280,7 @@
updatePrioritySaveButton();
loadDuplicates(); // Refresh the display
} else {
alert(`❌ Error: ${result.error}`);
alert('❌ Error: ' + result.error);
}
} catch (error) {
@ -1138,23 +1291,138 @@
}
// 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}`;
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(filePath);
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}`);
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}`;
videoPlayer.src = '/api/video/' + encodedPath;
// Show modal
modal.style.display = 'block';
@ -1162,14 +1430,40 @@
// 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');
alert('Error loading video. The file may not exist or may not be accessible. Check console for details.');
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
@ -1183,41 +1477,119 @@
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
// 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 -->