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'
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Main dashboard page."""
|
"""Main dashboard page."""
|
||||||
@ -538,54 +634,83 @@ def serve_video(file_path):
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
decoded_path = urllib.parse.unquote(file_path)
|
decoded_path = urllib.parse.unquote(file_path)
|
||||||
|
|
||||||
|
# Normalize the path to fix any malformed paths
|
||||||
|
normalized_path = normalize_path(decoded_path)
|
||||||
|
|
||||||
# Debug logging
|
# Debug logging
|
||||||
print(f"DEBUG: Video request for path: {decoded_path}")
|
print(f"DEBUG: Video request for path: {decoded_path}")
|
||||||
|
print(f"DEBUG: Normalized path: {normalized_path}")
|
||||||
print(f"DEBUG: Current working directory: {os.getcwd()}")
|
print(f"DEBUG: Current working directory: {os.getcwd()}")
|
||||||
|
|
||||||
# Security check: ensure the path is within allowed directories
|
# Security check: ensure the path is within allowed directories
|
||||||
# This prevents directory traversal attacks
|
# This prevents directory traversal attacks
|
||||||
if '..' in decoded_path:
|
if '..' in normalized_path:
|
||||||
print(f"DEBUG: Security check failed - path contains '..'")
|
print(f"DEBUG: Security check failed - path contains '..'")
|
||||||
return jsonify({'error': 'Invalid file path'}), 400
|
return jsonify({'error': 'Invalid file path'}), 400
|
||||||
|
|
||||||
# On Windows, allow absolute paths with drive letters
|
# On Windows, allow absolute paths with drive letters
|
||||||
# On Unix-like systems, block absolute paths
|
# On Unix-like systems, block absolute paths
|
||||||
if os.name == 'nt': # Windows
|
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")
|
print(f"DEBUG: Security check failed - Unix-style absolute path on Windows")
|
||||||
return jsonify({'error': 'Invalid file path'}), 400
|
return jsonify({'error': 'Invalid file path'}), 400
|
||||||
else: # Unix-like systems
|
else: # Unix-like systems
|
||||||
if decoded_path.startswith('/'):
|
if normalized_path.startswith('/'):
|
||||||
print(f"DEBUG: Security check failed - absolute path on Unix")
|
print(f"DEBUG: Security check failed - absolute path on Unix")
|
||||||
return jsonify({'error': 'Invalid file path'}), 400
|
return jsonify({'error': 'Invalid file path'}), 400
|
||||||
|
|
||||||
# Check if file exists
|
# Check if file exists
|
||||||
if not os.path.exists(decoded_path):
|
if not os.path.exists(normalized_path):
|
||||||
print(f"DEBUG: File does not exist: {decoded_path}")
|
print(f"DEBUG: File does not exist: {normalized_path}")
|
||||||
return jsonify({'error': 'Video file not found'}), 404
|
return jsonify({'error': 'Video file not found'}), 404
|
||||||
|
|
||||||
# Check if it's a video file
|
# Check if it's a video file and determine MIME type
|
||||||
if not decoded_path.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv')):
|
file_extension = os.path.splitext(normalized_path)[1].lower()
|
||||||
print(f"DEBUG: Invalid file type: {decoded_path}")
|
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
|
return jsonify({'error': 'Invalid file type'}), 400
|
||||||
|
|
||||||
|
mime_type = mime_types[file_extension]
|
||||||
|
|
||||||
# Get file info for debugging
|
# 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: File exists, size: {file_size} bytes")
|
||||||
|
print(f"DEBUG: MIME type: {mime_type}")
|
||||||
|
|
||||||
# Serve the video file
|
# Serve the video file
|
||||||
directory = os.path.dirname(decoded_path)
|
directory = os.path.dirname(normalized_path)
|
||||||
filename = os.path.basename(decoded_path)
|
filename = os.path.basename(normalized_path)
|
||||||
|
|
||||||
print(f"DEBUG: Serving from directory: {directory}")
|
print(f"DEBUG: Serving from directory: {directory}")
|
||||||
print(f"DEBUG: Filename: {filename}")
|
print(f"DEBUG: Filename: {filename}")
|
||||||
|
|
||||||
return send_from_directory(
|
# Add headers for better video streaming
|
||||||
|
response = send_from_directory(
|
||||||
directory,
|
directory,
|
||||||
filename,
|
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:
|
except Exception as e:
|
||||||
print(f"DEBUG: Exception in serve_video: {str(e)}")
|
print(f"DEBUG: Exception in serve_video: {str(e)}")
|
||||||
return jsonify({'error': f'Error serving video: {str(e)}'}), 500
|
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://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">
|
<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 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>
|
<style>
|
||||||
.duplicate-card {
|
.duplicate-card {
|
||||||
border-left: 4px solid #dc3545;
|
border-left: 4px solid #dc3545;
|
||||||
@ -112,9 +128,17 @@
|
|||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
.drag-handle:hover {
|
.drag-handle:hover {
|
||||||
color: #495057;
|
color: #495057;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
background-color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.priority-info {
|
.priority-info {
|
||||||
@ -379,6 +403,9 @@
|
|||||||
<button class="btn btn-success w-100" onclick="saveChanges()" id="save-btn" disabled>
|
<button class="btn btn-success w-100" onclick="saveChanges()" id="save-btn" disabled>
|
||||||
<i class="fas fa-save"></i> Save Changes
|
<i class="fas fa-save"></i> Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
<small class="text-muted" id="drag-status">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> Loading drag-and-drop...
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
@ -496,6 +523,14 @@
|
|||||||
|
|
||||||
// Load data on page load
|
// Load data on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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();
|
loadStats();
|
||||||
loadArtists();
|
loadArtists();
|
||||||
loadPriorityPreferences();
|
loadPriorityPreferences();
|
||||||
@ -516,7 +551,7 @@
|
|||||||
|
|
||||||
// Calculate space savings percentage
|
// Calculate space savings percentage
|
||||||
const savingsPercent = ((data.total_files_to_skip / data.total_songs) * 100).toFixed(1);
|
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
|
// Current file types
|
||||||
document.getElementById('total-mp4').textContent = data.total_file_types.MP4.toLocaleString();
|
document.getElementById('total-mp4').textContent = data.total_file_types.MP4.toLocaleString();
|
||||||
@ -536,7 +571,7 @@
|
|||||||
Object.keys(data.channels).forEach(channel => {
|
Object.keys(data.channels).forEach(channel => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = channel.toLowerCase();
|
option.value = channel.toLowerCase();
|
||||||
option.textContent = `${channel} (${data.channels[channel]})`;
|
option.textContent = channel + ' (' + data.channels[channel] + ')';
|
||||||
channelSelect.appendChild(option);
|
channelSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -559,7 +594,7 @@
|
|||||||
...currentFilters
|
...currentFilters
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch(`/api/duplicates?${params}`);
|
const response = await fetch('/api/duplicates?' + params);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
currentPage = data.page;
|
currentPage = data.page;
|
||||||
@ -579,14 +614,14 @@
|
|||||||
|
|
||||||
|
|
||||||
function toggleDetails(songKey) {
|
function toggleDetails(songKey) {
|
||||||
const details = document.getElementById(`details-${songKey}`);
|
const details = document.getElementById('details-' + songKey);
|
||||||
if (!details) {
|
if (!details) {
|
||||||
console.error('Details element not found for:', songKey);
|
console.error('Details element not found for:', songKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the button that was clicked
|
// Find the button that was clicked
|
||||||
const button = document.querySelector(`[onclick="toggleDetails('${songKey}')"]`);
|
const button = document.querySelector('[onclick="toggleDetails(\'' + songKey.replace(/'/g, "\\'") + '\')"]');
|
||||||
if (!button) {
|
if (!button) {
|
||||||
console.error('Button not found for:', songKey);
|
console.error('Button not found for:', songKey);
|
||||||
return;
|
return;
|
||||||
@ -611,15 +646,15 @@
|
|||||||
const info = document.getElementById('pagination-info');
|
const info = document.getElementById('pagination-info');
|
||||||
const start = (page - 1) * perPage + 1;
|
const start = (page - 1) * perPage + 1;
|
||||||
const end = Math.min(page * perPage, total);
|
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');
|
const pagination = document.getElementById('pagination');
|
||||||
pagination.innerHTML = '';
|
pagination.innerHTML = '';
|
||||||
|
|
||||||
// Previous button
|
// Previous button
|
||||||
const prevLi = document.createElement('li');
|
const prevLi = document.createElement('li');
|
||||||
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
|
prevLi.className = 'page-item ' + (page === 1 ? 'disabled' : '');
|
||||||
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${page - 1})">Previous</a>`;
|
prevLi.innerHTML = '<a class="page-link" href="#" onclick="loadDuplicates(' + (page - 1) + ')">Previous</a>';
|
||||||
pagination.appendChild(prevLi);
|
pagination.appendChild(prevLi);
|
||||||
|
|
||||||
// Page numbers
|
// Page numbers
|
||||||
@ -628,15 +663,15 @@
|
|||||||
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = `page-item ${i === page ? 'active' : ''}`;
|
li.className = 'page-item ' + (i === page ? 'active' : '');
|
||||||
li.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${i})">${i}</a>`;
|
li.innerHTML = '<a class="page-link" href="#" onclick="loadDuplicates(' + i + ')">' + i + '</a>';
|
||||||
pagination.appendChild(li);
|
pagination.appendChild(li);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next button
|
// Next button
|
||||||
const nextLi = document.createElement('li');
|
const nextLi = document.createElement('li');
|
||||||
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
|
nextLi.className = 'page-item ' + (page === totalPages ? 'disabled' : '');
|
||||||
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadDuplicates(${page + 1})">Next</a>`;
|
nextLi.innerHTML = '<a class="page-link" href="#" onclick="loadDuplicates(' + (page + 1) + ')">Next</a>';
|
||||||
pagination.appendChild(nextLi);
|
pagination.appendChild(nextLi);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,7 +758,7 @@
|
|||||||
allArtists.forEach(artist => {
|
allArtists.forEach(artist => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = artist.name;
|
option.value = artist.name;
|
||||||
option.textContent = `${artist.name} (${artist.total_duplicates} duplicates)`;
|
option.textContent = artist.name + ' (' + artist.total_duplicates + ' duplicates)';
|
||||||
artistSelect.appendChild(option);
|
artistSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -774,7 +809,7 @@
|
|||||||
updateSaveButton();
|
updateSaveButton();
|
||||||
|
|
||||||
// Visual feedback
|
// Visual feedback
|
||||||
const element = document.querySelector(`[data-path="${filePath}"]`);
|
const element = document.querySelector('[data-path="' + filePath.replace(/"/g, '\\"') + '"]');
|
||||||
if (element) {
|
if (element) {
|
||||||
element.style.opacity = '0.5';
|
element.style.opacity = '0.5';
|
||||||
element.style.backgroundColor = '#d4edda';
|
element.style.backgroundColor = '#d4edda';
|
||||||
@ -785,7 +820,7 @@
|
|||||||
const saveBtn = document.getElementById('save-btn');
|
const saveBtn = document.getElementById('save-btn');
|
||||||
if (pendingChanges.length > 0) {
|
if (pendingChanges.length > 0) {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.textContent = `Save Changes (${pendingChanges.length})`;
|
saveBtn.textContent = 'Save Changes (' + pendingChanges.length + ')';
|
||||||
} else {
|
} else {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
saveBtn.textContent = 'Save Changes';
|
saveBtn.textContent = 'Save Changes';
|
||||||
@ -812,12 +847,12 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(`✅ ${result.message}`);
|
alert('✅ ' + result.message);
|
||||||
pendingChanges = [];
|
pendingChanges = [];
|
||||||
updateSaveButton();
|
updateSaveButton();
|
||||||
loadDuplicates(); // Refresh the data
|
loadDuplicates(); // Refresh the data
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ Error: ${result.error}`);
|
alert('❌ Error: ' + result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -842,7 +877,10 @@
|
|||||||
|
|
||||||
// Initialize sortable for all duplicate groups
|
// Initialize sortable for all duplicate groups
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('Initializing sortable instances...');
|
||||||
|
updateDragStatus('loading', 'Initializing drag-and-drop...');
|
||||||
initializeSortable();
|
initializeSortable();
|
||||||
|
console.log('Sortable initialization complete');
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -867,19 +905,19 @@
|
|||||||
// Sort artists alphabetically
|
// Sort artists alphabetically
|
||||||
const sortedArtists = Object.values(artists).sort((a, b) => a.name.localeCompare(b.name));
|
const sortedArtists = Object.values(artists).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
container.innerHTML = sortedArtists.map(artist => `
|
container.innerHTML = sortedArtists.map(artist =>
|
||||||
<div class="card mb-4">
|
'<div class="card mb-4">' +
|
||||||
<div class="card-header bg-primary text-white">
|
'<div class="card-header bg-primary text-white">' +
|
||||||
<h5 class="mb-0">
|
'<h5 class="mb-0">' +
|
||||||
<i class="fas fa-user"></i> ${artist.name}
|
'<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>
|
'<span class="badge bg-light text-dark ms-2">' + artist.songs.length + ' songs, ' + artist.totalDuplicates + ' duplicates</span>' +
|
||||||
</h5>
|
'</h5>' +
|
||||||
</div>
|
'</div>' +
|
||||||
<div class="card-body">
|
'<div class="card-body">' +
|
||||||
${artist.songs.map(duplicate => createSongCard(duplicate)).join('')}
|
artist.songs.map(duplicate => createSongCard(duplicate)).join('') +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>'
|
||||||
`).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayAllSongsView(duplicates) {
|
function displayAllSongsView(duplicates) {
|
||||||
@ -889,10 +927,10 @@
|
|||||||
|
|
||||||
function createSongCard(duplicate) {
|
function createSongCard(duplicate) {
|
||||||
// Create a safe ID by replacing special characters
|
// 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
|
// Get current priority order for this song
|
||||||
const songKey = `${duplicate.artist} - ${duplicate.title}`;
|
const songKey = duplicate.artist + ' - ' + duplicate.title;
|
||||||
const currentPriorities = priorityChanges[songKey] || [];
|
const currentPriorities = priorityChanges[songKey] || [];
|
||||||
|
|
||||||
// Create all versions array (kept + skipped)
|
// Create all versions array (kept + skipped)
|
||||||
@ -918,61 +956,59 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return '<div class="card duplicate-card">' +
|
||||||
<div class="card duplicate-card">
|
'<div class="card-header">' +
|
||||||
<div class="card-header">
|
'<div class="d-flex justify-content-between align-items-center">' +
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
'<h6 class="mb-0">' +
|
||||||
<h6 class="mb-0">
|
'<strong>' + duplicate.artist + ' - ' + duplicate.title + '</strong>' +
|
||||||
<strong>${duplicate.artist} - ${duplicate.title}</strong>
|
'<span class="badge bg-primary ms-2">' + duplicate.total_duplicates + ' duplicates</span>' +
|
||||||
<span class="badge bg-primary ms-2">${duplicate.total_duplicates} duplicates</span>
|
'</h6>' +
|
||||||
</h6>
|
'<div>' +
|
||||||
<div>
|
'<button class="btn btn-sm btn-outline-secondary me-2" onclick="toggleDetails(\'' + safeId.replace(/'/g, "\\'") + '\')">' +
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" onclick="toggleDetails('${safeId}')">
|
'<i class="fas fa-chevron-down"></i> Details' +
|
||||||
<i class="fas fa-chevron-down"></i> Details
|
'</button>' +
|
||||||
</button>
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'<div class="card-body" id="details-' + safeId + '" style="display: none;">' +
|
||||||
<div class="card-body" id="details-${safeId}" style="display: none;">
|
'<div class="priority-info">' +
|
||||||
<div class="priority-info">
|
'<i class="fas fa-info-circle"></i> ' +
|
||||||
<i class="fas fa-info-circle"></i>
|
'<strong>Drag and drop to reorder file priorities.</strong> ' +
|
||||||
<strong>Drag and drop to reorder file priorities.</strong>
|
'The top file will be kept, others will be skipped. ' +
|
||||||
The top file will be kept, others will be skipped.
|
'Click "Save Priority Preferences" to apply these changes for future CLI runs.' +
|
||||||
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>
|
'</div>' +
|
||||||
|
'<!-- Sortable Versions List -->' +
|
||||||
<!-- Sortable Versions List -->
|
'<h6><i class="fas fa-sort"></i> FILE PRIORITIES (Drag to reorder):</h6>' +
|
||||||
<h6><i class="fas fa-sort"></i> FILE PRIORITIES (Drag to reorder):</h6>
|
'<div class="sortable-list" id="sortable-' + safeId + '">' +
|
||||||
<div class="sortable-list" id="sortable-${safeId}">
|
allVersions.map((version, index) =>
|
||||||
${allVersions.map((version, index) => `
|
'<div class="card mb-2 sortable-item ' + (version.is_kept ? 'kept-version' : 'skipped-version') + '" ' +
|
||||||
<div class="card mb-2 sortable-item ${version.is_kept ? 'kept-version' : 'skipped-version'}"
|
'data-path="' + version.path + '" data-index="' + index + '">' +
|
||||||
data-path="${version.path}" data-index="${index}">
|
'<div class="priority-indicator priority-' + Math.min(index + 1, 5) + '">' + (index + 1) + '</div>' +
|
||||||
<div class="priority-indicator priority-${Math.min(index + 1, 5)}">${index + 1}</div>
|
'<div class="card-body">' +
|
||||||
<div class="card-body">
|
'<div class="d-flex align-items-start">' +
|
||||||
<div class="d-flex align-items-start">
|
'<div class="drag-handle">' +
|
||||||
<div class="drag-handle">
|
'<i class="fas fa-grip-vertical"></i>' +
|
||||||
<i class="fas fa-grip-vertical"></i>
|
'</div>' +
|
||||||
</div>
|
'<div class="flex-grow-1">' +
|
||||||
<div class="flex-grow-1">
|
'<div class="path-text">' + version.path + '</div>' +
|
||||||
<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-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>' +
|
||||||
<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.is_kept ? '<span class="badge bg-success ms-1">KEPT</span>' : '<span class="badge bg-danger ms-1">SKIPPED</span>'}
|
(version.file_type === 'MP4' ?
|
||||||
${version.file_type === 'MP4' ?
|
'<button class="play-button" onclick="openVideoPlayer(\'' + version.path.replace(/'/g, "\\'") + '\', \'' + duplicate.artist.replace(/'/g, "\\'") + '\', \'' + duplicate.title.replace(/'/g, "\\'") + '\')">' +
|
||||||
`<button class="play-button" onclick="openVideoPlayer('${version.path}', '${duplicate.artist.replace(/'/g, "\\'")}', '${duplicate.title.replace(/'/g, "\\'")}')">
|
'<i class="fas fa-play"></i> Play' +
|
||||||
<i class="fas fa-play"></i> Play
|
'</button>' :
|
||||||
</button>` :
|
''
|
||||||
''
|
) +
|
||||||
}
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>'
|
||||||
</div>
|
).join('') +
|
||||||
`).join('')}
|
'</div>' +
|
||||||
</div>
|
'</div>' +
|
||||||
</div>
|
'</div>';
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadMp3Songs() {
|
async function downloadMp3Songs() {
|
||||||
@ -1020,39 +1056,156 @@
|
|||||||
// Priority Management Functions
|
// Priority Management Functions
|
||||||
function initializeSortable() {
|
function initializeSortable() {
|
||||||
// Destroy existing instances
|
// Destroy existing instances
|
||||||
sortableInstances.forEach(instance => instance.destroy());
|
sortableInstances.forEach(instance => {
|
||||||
|
if (instance && typeof instance.destroy === 'function') {
|
||||||
|
instance.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
sortableInstances = [];
|
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
|
// Initialize new sortable instances
|
||||||
document.querySelectorAll('.sortable-list').forEach(list => {
|
document.querySelectorAll('.sortable-list').forEach(list => {
|
||||||
const sortable = Sortable.create(list, {
|
try {
|
||||||
handle: '.drag-handle',
|
console.log('Initializing sortable for list:', list.id);
|
||||||
animation: 150,
|
const sortable = Sortable.create(list, {
|
||||||
ghostClass: 'sortable-ghost',
|
handle: '.drag-handle',
|
||||||
chosenClass: 'sortable-chosen',
|
animation: 150,
|
||||||
onEnd: function(evt) {
|
ghostClass: 'sortable-ghost',
|
||||||
const songKey = getSongKeyFromSortableList(evt.to);
|
chosenClass: 'sortable-chosen',
|
||||||
updatePriorityOrder(songKey, evt.to);
|
onStart: function(evt) {
|
||||||
updatePriorityIndicators(evt.to);
|
console.log('Sortable started:', evt);
|
||||||
}
|
},
|
||||||
});
|
onEnd: function(evt) {
|
||||||
sortableInstances.push(sortable);
|
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) {
|
function getSongKeyFromSortableList(listElement) {
|
||||||
const detailsElement = listElement.closest('.card-body');
|
try {
|
||||||
const cardElement = detailsElement.closest('.duplicate-card');
|
const detailsElement = listElement.closest('.card-body');
|
||||||
const titleElement = cardElement.querySelector('h6 strong');
|
const cardElement = detailsElement.closest('.duplicate-card');
|
||||||
return titleElement.textContent;
|
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) {
|
function updatePriorityOrder(songKey, listElement) {
|
||||||
const items = Array.from(listElement.querySelectorAll('.sortable-item'));
|
try {
|
||||||
const newOrder = items.map(item => item.getAttribute('data-path'));
|
const items = Array.from(listElement.querySelectorAll('.sortable-item'));
|
||||||
|
const newOrder = items.map(item => item.getAttribute('data-path'));
|
||||||
priorityChanges[songKey] = newOrder;
|
|
||||||
updatePrioritySaveButton();
|
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) {
|
function updatePriorityIndicators(listElement) {
|
||||||
@ -1060,7 +1213,7 @@
|
|||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const indicator = item.querySelector('.priority-indicator');
|
const indicator = item.querySelector('.priority-indicator');
|
||||||
if (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;
|
indicator.textContent = index + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1072,7 +1225,7 @@
|
|||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.textContent = `Save Priority Preferences (${Object.keys(priorityChanges).length} songs)`;
|
saveBtn.textContent = 'Save Priority Preferences (' + Object.keys(priorityChanges).length + ' songs)';
|
||||||
} else {
|
} else {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
saveBtn.textContent = 'Save Priority Preferences';
|
saveBtn.textContent = 'Save Priority Preferences';
|
||||||
@ -1099,11 +1252,11 @@
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert(`✅ Priority preferences saved successfully!\n\n${result.message}`);
|
alert('✅ Priority preferences saved successfully!\n\n' + result.message);
|
||||||
priorityChanges = {};
|
priorityChanges = {};
|
||||||
updatePrioritySaveButton();
|
updatePrioritySaveButton();
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ Error: ${result.error}`);
|
alert('❌ Error: ' + result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1127,7 +1280,7 @@
|
|||||||
updatePrioritySaveButton();
|
updatePrioritySaveButton();
|
||||||
loadDuplicates(); // Refresh the display
|
loadDuplicates(); // Refresh the display
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ Error: ${result.error}`);
|
alert('❌ Error: ' + result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1138,23 +1291,138 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Video Player Functions
|
// 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) {
|
function openVideoPlayer(filePath, artist, title) {
|
||||||
const modal = document.getElementById('videoModal');
|
const modal = document.getElementById('videoModal');
|
||||||
const videoPlayer = document.getElementById('videoPlayer');
|
const videoPlayer = document.getElementById('videoPlayer');
|
||||||
const modalTitle = document.getElementById('videoModalTitle');
|
const modalTitle = document.getElementById('videoModalTitle');
|
||||||
|
|
||||||
// Set modal title
|
// 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
|
// 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: Opening video player for:', filePath);
|
||||||
|
console.log('DEBUG: Normalized path:', normalizedPath);
|
||||||
console.log('DEBUG: Encoded path:', encodedPath);
|
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
|
// Set video source using Flask route
|
||||||
videoPlayer.src = `/api/video/${encodedPath}`;
|
videoPlayer.src = '/api/video/' + encodedPath;
|
||||||
|
|
||||||
// Show modal
|
// Show modal
|
||||||
modal.style.display = 'block';
|
modal.style.display = 'block';
|
||||||
@ -1162,14 +1430,40 @@
|
|||||||
// Add event listener for video load
|
// Add event listener for video load
|
||||||
videoPlayer.onloadeddata = function() {
|
videoPlayer.onloadeddata = function() {
|
||||||
console.log('Video loaded successfully');
|
console.log('Video loaded successfully');
|
||||||
|
console.log('Video duration:', videoPlayer.duration);
|
||||||
|
console.log('Video ready state:', videoPlayer.readyState);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add error handling
|
// Add error handling
|
||||||
videoPlayer.onerror = function(e) {
|
videoPlayer.onerror = function(e) {
|
||||||
console.error('Error loading video:', filePath);
|
console.error('Error loading video:', filePath);
|
||||||
|
console.error('Normalized path:', normalizedPath);
|
||||||
console.error('Video error details:', e);
|
console.error('Video error details:', e);
|
||||||
console.error('Video error code:', videoPlayer.error ? videoPlayer.error.code : 'unknown');
|
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
|
// Add more detailed event listeners
|
||||||
@ -1183,41 +1477,119 @@
|
|||||||
|
|
||||||
videoPlayer.oncanplay = function() {
|
videoPlayer.oncanplay = function() {
|
||||||
console.log('Video can start playing');
|
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() {
|
videoPlayer.onabort = function() {
|
||||||
console.log('Video loading aborted');
|
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() {
|
function closeVideoPlayer() {
|
||||||
const modal = document.getElementById('videoModal');
|
try {
|
||||||
const videoPlayer = document.getElementById('videoPlayer');
|
const modal = document.getElementById('videoModal');
|
||||||
|
const videoPlayer = document.getElementById('videoPlayer');
|
||||||
// Pause video
|
|
||||||
videoPlayer.pause();
|
if (videoPlayer) {
|
||||||
|
// Pause video
|
||||||
// Clear source
|
videoPlayer.pause();
|
||||||
videoPlayer.src = '';
|
|
||||||
|
// Clear source and remove all event listeners
|
||||||
// Hide modal
|
videoPlayer.src = '';
|
||||||
modal.style.display = 'none';
|
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
|
// Close modal when clicking outside
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('videoModal');
|
try {
|
||||||
if (event.target === modal) {
|
const modal = document.getElementById('videoModal');
|
||||||
closeVideoPlayer();
|
if (event.target === modal) {
|
||||||
|
closeVideoPlayer();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in window click handler:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal with Escape key
|
// Close modal with Escape key
|
||||||
document.addEventListener('keydown', function(event) {
|
document.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Escape') {
|
try {
|
||||||
closeVideoPlayer();
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- Video Player Modal -->
|
<!-- Video Player Modal -->
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user