Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
16745f5270
commit
54bce66584
151
web/app.py
151
web/app.py
@ -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
|
||||
|
||||
@ -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 -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user