From 16745f527038795192cdcd14ca0c5bad162b3986 Mon Sep 17 00:00:00 2001 From: mbrucedogs Date: Sat, 26 Jul 2025 17:23:31 -0500 Subject: [PATCH] Signed-off-by: mbrucedogs --- PRD.md | 8 +- web/app.py | 60 ++++++++++++ web/templates/index.html | 199 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) diff --git a/PRD.md b/PRD.md index ba912e4..0af6c16 100644 --- a/PRD.md +++ b/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 diff --git a/web/app.py b/web/app.py index 5e15ae1..bfc3880 100644 --- a/web/app.py +++ b/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/') +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) \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html index 486d823..36e6858 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -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; + } @@ -862,6 +958,12 @@ ${version.file_type} ${version.channel} ${version.is_kept ? 'KEPT' : 'SKIPPED'} + ${version.file_type === 'MP4' ? + `` : + '' + } @@ -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(); + } + }); + + +
+
+
+
Video Player
+ +
+
+ +
+
+
\ No newline at end of file