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
|
- **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
|
||||||
|
|||||||
60
web/app.py
60
web/app.py
@ -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)
|
||||||
@ -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()">×</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>
|
||||||
Loading…
Reference in New Issue
Block a user