Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
3969d75f0f
commit
16745f5270
8
PRD.md
8
PRD.md
@ -254,6 +254,8 @@ KaraokeMerge/
|
||||
- **Search & Filter**: Real-time search across artists, titles, and paths
|
||||
- **Responsive Design**: Mobile-friendly interface
|
||||
- **Easy Startup**: Automated dependency checking and browser launch
|
||||
- **Video Playback**: Direct MP4 video playback in modal popup for previewing karaoke videos
|
||||
- **Drag-and-Drop Priority Management**: Interactive reordering of file priorities with persistent preferences
|
||||
|
||||
### 7.2 Web UI Architecture
|
||||
- **Flask Backend**: Lightweight web server (`web/app.py`)
|
||||
@ -261,10 +263,12 @@ KaraokeMerge/
|
||||
- **Startup Script**: Dependency management and server startup (`start_web_ui.py`)
|
||||
|
||||
### 7.3 Future Web UI Enhancements
|
||||
- Embedded media player for audio/video preview
|
||||
- Audio preview for MP3 files
|
||||
- Real-time configuration editing
|
||||
- Advanced filtering and sorting options
|
||||
- Export capabilities for manual selections
|
||||
- Batch video preview functionality
|
||||
- Video thumbnail generation
|
||||
|
||||
---
|
||||
|
||||
@ -342,6 +346,8 @@ The tool has been successfully implemented with the following components:
|
||||
- [x] Create startup script for web UI with dependency checking
|
||||
- [x] Add comprehensive .gitignore file
|
||||
- [x] Update documentation with required data file information
|
||||
- [x] Implement drag-and-drop priority management with persistent preferences
|
||||
- [x] Add MP4 video playback functionality in web UI modal
|
||||
|
||||
#### 🎯 **Next Priority Items**
|
||||
- [ ] Analyze MP4 files without channel priorities to suggest new folder names
|
||||
|
||||
60
web/app.py
60
web/app.py
@ -530,5 +530,65 @@ def load_priority_preferences():
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Error loading priority preferences: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/video/<path:file_path>')
|
||||
def serve_video(file_path):
|
||||
"""Serve video files for playback in the web UI."""
|
||||
try:
|
||||
# Decode the file path (it comes URL-encoded)
|
||||
import urllib.parse
|
||||
decoded_path = urllib.parse.unquote(file_path)
|
||||
|
||||
# Debug logging
|
||||
print(f"DEBUG: Video request for path: {decoded_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:
|
||||
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(':'):
|
||||
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('/'):
|
||||
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}")
|
||||
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}")
|
||||
return jsonify({'error': 'Invalid file type'}), 400
|
||||
|
||||
# Get file info for debugging
|
||||
file_size = os.path.getsize(decoded_path)
|
||||
print(f"DEBUG: File exists, size: {file_size} bytes")
|
||||
|
||||
# Serve the video file
|
||||
directory = os.path.dirname(decoded_path)
|
||||
filename = os.path.basename(decoded_path)
|
||||
|
||||
print(f"DEBUG: Serving from directory: {directory}")
|
||||
print(f"DEBUG: Filename: {filename}")
|
||||
|
||||
return send_from_directory(
|
||||
directory,
|
||||
filename,
|
||||
mimetype='video/mp4' # Adjust based on file type if needed
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Exception in serve_video: {str(e)}")
|
||||
return jsonify({'error': f'Error serving video: {str(e)}'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
@ -125,6 +125,102 @@
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Video Player Modal Styles */
|
||||
.video-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1050;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.video-modal-content {
|
||||
position: relative;
|
||||
margin: 2% auto;
|
||||
padding: 0;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
background-color: #000;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.video-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #343a40;
|
||||
color: white;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.video-modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.video-modal-close {
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.video-modal-close:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.play-button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-path-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -862,6 +958,12 @@
|
||||
<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>` :
|
||||
''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1034,6 +1136,103 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Video Player Functions
|
||||
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}`;
|
||||
|
||||
// Encode the file path for the URL
|
||||
const encodedPath = encodeURIComponent(filePath);
|
||||
|
||||
console.log('DEBUG: Opening video player for:', filePath);
|
||||
console.log('DEBUG: Encoded path:', encodedPath);
|
||||
console.log('DEBUG: Video URL:', `/api/video/${encodedPath}`);
|
||||
|
||||
// Set video source using Flask route
|
||||
videoPlayer.src = `/api/video/${encodedPath}`;
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
|
||||
// Add event listener for video load
|
||||
videoPlayer.onloadeddata = function() {
|
||||
console.log('Video loaded successfully');
|
||||
};
|
||||
|
||||
// Add error handling
|
||||
videoPlayer.onerror = function(e) {
|
||||
console.error('Error loading video:', filePath);
|
||||
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.');
|
||||
};
|
||||
|
||||
// Add more detailed event listeners
|
||||
videoPlayer.onloadstart = function() {
|
||||
console.log('Video load started');
|
||||
};
|
||||
|
||||
videoPlayer.onprogress = function() {
|
||||
console.log('Video loading progress');
|
||||
};
|
||||
|
||||
videoPlayer.oncanplay = function() {
|
||||
console.log('Video can start playing');
|
||||
};
|
||||
|
||||
videoPlayer.onabort = function() {
|
||||
console.log('Video loading aborted');
|
||||
};
|
||||
}
|
||||
|
||||
function closeVideoPlayer() {
|
||||
const modal = document.getElementById('videoModal');
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
|
||||
// Pause video
|
||||
videoPlayer.pause();
|
||||
|
||||
// Clear source
|
||||
videoPlayer.src = '';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('videoModal');
|
||||
if (event.target === modal) {
|
||||
closeVideoPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeVideoPlayer();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Video Player Modal -->
|
||||
<div id="videoModal" class="video-modal">
|
||||
<div class="video-modal-content">
|
||||
<div class="video-modal-header">
|
||||
<h5 class="video-modal-title" id="videoModalTitle">Video Player</h5>
|
||||
<button class="video-modal-close" onclick="closeVideoPlayer()">×</button>
|
||||
</div>
|
||||
<div class="video-container">
|
||||
<video id="videoPlayer" class="video-player" controls>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user