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

This commit is contained in:
mbrucedogs 2025-07-26 17:23:31 -05:00
parent 3969d75f0f
commit 16745f5270
3 changed files with 266 additions and 1 deletions

8
PRD.md
View File

@ -254,6 +254,8 @@ KaraokeMerge/
- **Search & Filter**: Real-time search across artists, titles, and paths - **Search & Filter**: Real-time search across artists, titles, and paths
- **Responsive Design**: Mobile-friendly interface - **Responsive Design**: Mobile-friendly interface
- **Easy Startup**: Automated dependency checking and browser launch - **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 ### 7.2 Web UI Architecture
- **Flask Backend**: Lightweight web server (`web/app.py`) - **Flask Backend**: Lightweight web server (`web/app.py`)
@ -261,10 +263,12 @@ KaraokeMerge/
- **Startup Script**: Dependency management and server startup (`start_web_ui.py`) - **Startup Script**: Dependency management and server startup (`start_web_ui.py`)
### 7.3 Future Web UI Enhancements ### 7.3 Future Web UI Enhancements
- Embedded media player for audio/video preview - Audio preview for MP3 files
- Real-time configuration editing - Real-time configuration editing
- Advanced filtering and sorting options - Advanced filtering and sorting options
- Export capabilities for manual selections - 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] Create startup script for web UI with dependency checking
- [x] Add comprehensive .gitignore file - [x] Add comprehensive .gitignore file
- [x] Update documentation with required data file information - [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** #### 🎯 **Next Priority Items**
- [ ] Analyze MP4 files without channel priorities to suggest new folder names - [ ] Analyze MP4 files without channel priorities to suggest new folder names

View File

@ -530,5 +530,65 @@ def load_priority_preferences():
except Exception as e: except Exception as e:
return jsonify({'error': f'Error loading priority preferences: {str(e)}'}), 500 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__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)

View File

@ -125,6 +125,102 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 0.9rem; 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> </style>
</head> </head>
<body> <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-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">KEPT</span>' : '<span class="badge bg-danger ms-1">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' ?
`<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> </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> </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()">&times;</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> </body>
</html> </html>