Implement working Porn Cinema media player with video library
- Added complete porn cinema media player with dedicated HTML page - Implemented PornCinema class with video playback, controls, keyboard shortcuts - Added VideoLibrary class with thumbnail generation and duration display - Canvas-based thumbnails with aspect ratio support for portrait/landscape videos - Playlist functionality with save/load, shuffle, auto-advance - Desktop file manager integration for video file access - Enhanced MIME type handling with fallback strategies for .mov files - Professional dark cinema theme with responsive design - One-handed keyboard controls for playback and navigation - Quality selection, theater mode, fullscreen support - Working video playback with proper file:// URL handling for Electron
This commit is contained in:
parent
c65a58bae9
commit
a2af9ba8f3
|
|
@ -198,7 +198,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Initialize cinema when page loads
|
// Initialize cinema when page loads
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
console.log('🎬 Initializing Porn Cinema...');
|
console.log('🎬 Initializing Porn Cinema...');
|
||||||
|
|
||||||
// Initialize desktop file manager if in Electron environment
|
// Initialize desktop file manager if in Electron environment
|
||||||
|
|
@ -219,18 +219,23 @@
|
||||||
|
|
||||||
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
|
window.desktopFileManager = new DesktopFileManager(minimalDataManager);
|
||||||
console.log('🖥️ Desktop File Manager initialized for porn cinema');
|
console.log('🖥️ Desktop File Manager initialized for porn cinema');
|
||||||
|
|
||||||
|
// Wait a moment for the desktop file manager to fully initialize
|
||||||
|
// The init() method is async and sets up video directories
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
} else if (!window.electronAPI) {
|
} else if (!window.electronAPI) {
|
||||||
console.warn('⚠️ Running in browser mode - video management limited');
|
console.warn('⚠️ Running in browser mode - video management limited');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the cinema
|
// Initialize the cinema after desktop file manager is ready
|
||||||
window.pornCinema = new PornCinema();
|
window.pornCinema = new PornCinema();
|
||||||
window.pornCinema.initialize();
|
await window.pornCinema.initialize();
|
||||||
|
|
||||||
// Hide loading overlay
|
// Hide loading overlay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('cinema-loading').style.display = 'none';
|
document.getElementById('cinema-loading').style.display = 'none';
|
||||||
}, 1500);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Back to home functionality
|
// Back to home functionality
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ class PornCinema {
|
||||||
this.volume = 0.7;
|
this.volume = 0.7;
|
||||||
this.playbackRate = 1.0;
|
this.playbackRate = 1.0;
|
||||||
this.currentQuality = 'auto';
|
this.currentQuality = 'auto';
|
||||||
|
this.shouldAutoPlay = false;
|
||||||
|
this.fallbackMimeTypes = null;
|
||||||
|
this.currentVideoSrc = null;
|
||||||
|
|
||||||
// Playlist
|
// Playlist
|
||||||
this.playlist = [];
|
this.playlist = [];
|
||||||
|
|
@ -176,16 +179,39 @@ class PornCinema {
|
||||||
|
|
||||||
async playVideo(video) {
|
async playVideo(video) {
|
||||||
try {
|
try {
|
||||||
console.log(`🎬 Playing video: ${video.name}`);
|
|
||||||
|
|
||||||
this.currentVideo = video;
|
this.currentVideo = video;
|
||||||
this.showLoading();
|
this.showLoading();
|
||||||
|
|
||||||
// Update video source
|
// Convert path to proper format for Electron
|
||||||
this.videoSource.src = video.path;
|
let videoSrc = video.path;
|
||||||
this.videoSource.type = `video/${video.format || 'mp4'}`;
|
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
||||||
|
// Absolute Windows path - convert to file:// URL
|
||||||
|
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
||||||
|
} else if (window.electronAPI && !video.path.startsWith('file://')) {
|
||||||
|
// Relative path in Electron - use as is
|
||||||
|
videoSrc = video.path;
|
||||||
|
}
|
||||||
|
|
||||||
// Load and play
|
// Check if browser can play this format
|
||||||
|
const canPlayResult = this.checkCanPlay(video.format || video.path);
|
||||||
|
console.log(`🎬 Playing ${video.name} (${video.format}) - Can play: ${canPlayResult.canPlay ? 'Yes' : 'No'}`);
|
||||||
|
|
||||||
|
// For .mov files, try multiple MIME types as fallback
|
||||||
|
const mimeTypes = this.getVideoMimeTypes(video.format || video.path);
|
||||||
|
console.log(`🎬 Trying MIME types: ${mimeTypes.join(', ')}`);
|
||||||
|
|
||||||
|
// Update video source - try primary MIME type first
|
||||||
|
this.videoSource.src = videoSrc;
|
||||||
|
this.videoSource.type = canPlayResult.mimeType || mimeTypes[0];
|
||||||
|
|
||||||
|
// Store fallback MIME types for error handling
|
||||||
|
this.fallbackMimeTypes = mimeTypes.slice(1);
|
||||||
|
this.currentVideoSrc = videoSrc;
|
||||||
|
|
||||||
|
// Set flag to auto-play when loaded
|
||||||
|
this.shouldAutoPlay = true;
|
||||||
|
|
||||||
|
// Load the video
|
||||||
this.videoElement.load();
|
this.videoElement.load();
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
|
|
@ -199,6 +225,53 @@ class PornCinema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkCanPlay(formatOrPath) {
|
||||||
|
let extension = formatOrPath;
|
||||||
|
if (formatOrPath.includes('.')) {
|
||||||
|
extension = formatOrPath.toLowerCase().split('.').pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testVideo = document.createElement('video');
|
||||||
|
const mimeTypes = this.getVideoMimeTypes(formatOrPath);
|
||||||
|
|
||||||
|
for (const mimeType of mimeTypes) {
|
||||||
|
const canPlay = testVideo.canPlayType(mimeType);
|
||||||
|
if (canPlay === 'probably' || canPlay === 'maybe') {
|
||||||
|
return { canPlay: true, mimeType };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canPlay: false, mimeType: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoMimeTypes(formatOrPath) {
|
||||||
|
let extension = formatOrPath;
|
||||||
|
if (formatOrPath.includes('.')) {
|
||||||
|
extension = formatOrPath.toLowerCase().split('.').pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (extension.toLowerCase()) {
|
||||||
|
case 'mp4':
|
||||||
|
return ['video/mp4'];
|
||||||
|
case 'webm':
|
||||||
|
return ['video/webm'];
|
||||||
|
case 'mov':
|
||||||
|
case 'qt':
|
||||||
|
// Try multiple MIME types for .mov files
|
||||||
|
return ['video/quicktime', 'video/mp4', 'video/x-quicktime'];
|
||||||
|
case 'avi':
|
||||||
|
return ['video/avi', 'video/x-msvideo'];
|
||||||
|
case 'mkv':
|
||||||
|
return ['video/x-matroska'];
|
||||||
|
case 'ogg':
|
||||||
|
return ['video/ogg'];
|
||||||
|
case 'm4v':
|
||||||
|
return ['video/mp4'];
|
||||||
|
default:
|
||||||
|
return ['video/mp4']; // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
togglePlayPause() {
|
togglePlayPause() {
|
||||||
if (!this.videoElement.src) {
|
if (!this.videoElement.src) {
|
||||||
// No video loaded, try to play first video from library or playlist
|
// No video loaded, try to play first video from library or playlist
|
||||||
|
|
@ -337,19 +410,31 @@ class PornCinema {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playlist management
|
// Playlist management
|
||||||
addToPlaylist(video) {
|
async addToPlaylist(video) {
|
||||||
if (!this.playlist.find(v => v.path === video.path)) {
|
if (!this.playlist.find(v => v.path === video.path)) {
|
||||||
this.playlist.push({...video});
|
// Create a copy of the video with duration loaded if missing
|
||||||
|
const videoWithDuration = {...video};
|
||||||
|
|
||||||
|
// If duration is missing or 0, try to load it
|
||||||
|
if (!videoWithDuration.duration || videoWithDuration.duration === 0) {
|
||||||
|
try {
|
||||||
|
videoWithDuration.duration = await this.getVideoDuration(video);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not load duration for ${video.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlist.push(videoWithDuration);
|
||||||
this.updatePlaylistDisplay();
|
this.updatePlaylistDisplay();
|
||||||
console.log(`➕ Added to playlist: ${video.name}`);
|
console.log(`➕ Added to playlist: ${video.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addCurrentToPlaylist() {
|
async addCurrentToPlaylist() {
|
||||||
if (this.currentVideo) {
|
if (this.currentVideo) {
|
||||||
this.addToPlaylist(this.currentVideo);
|
await this.addToPlaylist(this.currentVideo);
|
||||||
} else if (this.videoLibrary && this.videoLibrary.getSelectedVideo()) {
|
} else if (this.videoLibrary && this.videoLibrary.getSelectedVideo()) {
|
||||||
this.addToPlaylist(this.videoLibrary.getSelectedVideo());
|
await this.addToPlaylist(this.videoLibrary.getSelectedVideo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,6 +618,15 @@ class PornCinema {
|
||||||
|
|
||||||
onLoadedData() {
|
onLoadedData() {
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
|
|
||||||
|
// Auto-play if requested
|
||||||
|
if (this.shouldAutoPlay) {
|
||||||
|
this.shouldAutoPlay = false;
|
||||||
|
this.videoElement.play().catch(error => {
|
||||||
|
console.error('🎬 Error auto-playing video:', error);
|
||||||
|
this.showError('Failed to play video');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCanPlay() {
|
onCanPlay() {
|
||||||
|
|
@ -544,18 +638,21 @@ class PornCinema {
|
||||||
this.isPlaying = true;
|
this.isPlaying = true;
|
||||||
this.updatePlayButton();
|
this.updatePlayButton();
|
||||||
this.hidePlayOverlay();
|
this.hidePlayOverlay();
|
||||||
|
this.hideVideoOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPause() {
|
onPause() {
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
this.updatePlayButton();
|
this.updatePlayButton();
|
||||||
this.showPlayOverlay();
|
this.showPlayOverlay();
|
||||||
|
this.showVideoOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnded() {
|
onEnded() {
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
this.updatePlayButton();
|
this.updatePlayButton();
|
||||||
this.showPlayOverlay();
|
this.showPlayOverlay();
|
||||||
|
this.showVideoOverlay();
|
||||||
|
|
||||||
// Auto-play next video in playlist
|
// Auto-play next video in playlist
|
||||||
if (this.playlist.length > 0 && this.currentPlaylistIndex >= 0) {
|
if (this.playlist.length > 0 && this.currentPlaylistIndex >= 0) {
|
||||||
|
|
@ -573,9 +670,63 @@ class PornCinema {
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(event) {
|
onError(event) {
|
||||||
console.error('Video error:', event);
|
const video = event.target;
|
||||||
|
const error = video.error;
|
||||||
|
|
||||||
|
// Try fallback MIME types for .mov files
|
||||||
|
if (this.fallbackMimeTypes && this.fallbackMimeTypes.length > 0) {
|
||||||
|
const nextMimeType = this.fallbackMimeTypes.shift();
|
||||||
|
console.log(`🔄 Trying fallback MIME type: ${nextMimeType}`);
|
||||||
|
|
||||||
|
this.videoSource.type = nextMimeType;
|
||||||
|
this.videoElement.load();
|
||||||
|
return; // Don't show error yet, try the fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorMessage = 'Error loading video';
|
||||||
|
let detailedMessage = 'Unknown error';
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case error.MEDIA_ERR_ABORTED:
|
||||||
|
detailedMessage = 'Video loading was aborted';
|
||||||
|
break;
|
||||||
|
case error.MEDIA_ERR_NETWORK:
|
||||||
|
detailedMessage = 'Network error while loading video';
|
||||||
|
break;
|
||||||
|
case error.MEDIA_ERR_DECODE:
|
||||||
|
detailedMessage = 'Video format not supported or corrupted';
|
||||||
|
errorMessage = 'Unsupported video format';
|
||||||
|
break;
|
||||||
|
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||||
|
detailedMessage = 'Video format or codec not supported';
|
||||||
|
errorMessage = 'Video format not supported';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
detailedMessage = `Unknown error (code: ${error.code})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`🎬 Video error for ${this.currentVideo?.name || 'unknown'}:`, {
|
||||||
|
code: error?.code,
|
||||||
|
message: error?.message,
|
||||||
|
details: detailedMessage,
|
||||||
|
format: this.currentVideo?.format,
|
||||||
|
src: video.src,
|
||||||
|
mimeType: this.videoSource.type
|
||||||
|
});
|
||||||
|
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
this.showError('Error loading video');
|
this.showError(errorMessage);
|
||||||
|
|
||||||
|
// Reset fallback MIME types
|
||||||
|
this.fallbackMimeTypes = null;
|
||||||
|
|
||||||
|
// If it's a .mov file that failed, suggest alternatives
|
||||||
|
if (this.currentVideo?.format?.toLowerCase() === 'mov') {
|
||||||
|
console.warn('💡 .mov file failed to play. This format may contain codecs not supported by browsers.');
|
||||||
|
console.warn('💡 Consider converting .mov files to .mp4 for better compatibility.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onFullscreenChange() {
|
onFullscreenChange() {
|
||||||
|
|
@ -641,6 +792,14 @@ class PornCinema {
|
||||||
this.playOverlay.style.display = 'none';
|
this.playOverlay.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showVideoOverlay() {
|
||||||
|
this.videoOverlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideVideoOverlay() {
|
||||||
|
this.videoOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
showControls() {
|
showControls() {
|
||||||
this.controls.container.classList.add('visible');
|
this.controls.container.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
@ -714,6 +873,40 @@ class PornCinema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getVideoDuration(video) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Create a temporary video element to load metadata
|
||||||
|
const tempVideo = document.createElement('video');
|
||||||
|
tempVideo.preload = 'metadata';
|
||||||
|
|
||||||
|
// Convert path to proper format for Electron
|
||||||
|
let videoSrc = video.path;
|
||||||
|
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
||||||
|
// Absolute Windows path - convert to file:// URL
|
||||||
|
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempVideo.addEventListener('loadedmetadata', () => {
|
||||||
|
const duration = tempVideo.duration;
|
||||||
|
tempVideo.remove(); // Clean up
|
||||||
|
resolve(duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
tempVideo.addEventListener('error', (e) => {
|
||||||
|
tempVideo.remove(); // Clean up
|
||||||
|
reject(new Error(`Failed to load video metadata: ${e.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set timeout to avoid hanging
|
||||||
|
setTimeout(() => {
|
||||||
|
tempVideo.remove();
|
||||||
|
reject(new Error('Timeout loading video metadata'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
tempVideo.src = videoSrc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
formatDuration(seconds) {
|
formatDuration(seconds) {
|
||||||
if (!seconds || isNaN(seconds)) return '0:00';
|
if (!seconds || isNaN(seconds)) return '0:00';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,29 @@ class VideoLibrary {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for desktop file manager to be fully initialized
|
||||||
|
// Check if video directories have been set up
|
||||||
|
let retries = 0;
|
||||||
|
const maxRetries = 20; // Wait up to 2 seconds
|
||||||
|
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
if (window.desktopFileManager.videoDirectories &&
|
||||||
|
window.desktopFileManager.videoDirectories.background) {
|
||||||
|
console.log('✅ Desktop file manager video directories are ready');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⏳ Waiting for video directories to initialize... (${retries + 1}/${maxRetries})`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
retries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries >= maxRetries) {
|
||||||
|
console.warn('⚠️ Video directories not initialized after waiting, falling back to stored videos');
|
||||||
|
this.loadStoredVideos();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get video files from all categories
|
// Get video files from all categories
|
||||||
let allVideos = [];
|
let allVideos = [];
|
||||||
|
|
||||||
|
|
@ -109,11 +132,17 @@ class VideoLibrary {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`📁 Loaded ${this.videos.length} videos`);
|
console.log(`📁 Loaded ${this.videos.length} videos`);
|
||||||
|
if (this.videos.length > 0) {
|
||||||
|
console.log('📁 Sample video object:', this.videos[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply current filters and display
|
// Apply current filters and display
|
||||||
this.applyFiltersAndSort();
|
this.applyFiltersAndSort();
|
||||||
this.displayLibrary();
|
this.displayLibrary();
|
||||||
|
|
||||||
|
// Load durations for videos that don't have them
|
||||||
|
this.loadMissingDurations();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading video library:', error);
|
console.error('Error loading video library:', error);
|
||||||
this.displayEmptyLibrary('Error loading video library');
|
this.displayEmptyLibrary('Error loading video library');
|
||||||
|
|
@ -238,13 +267,16 @@ class VideoLibrary {
|
||||||
const duration = this.formatDuration(video.duration);
|
const duration = this.formatDuration(video.duration);
|
||||||
const fileSize = this.formatFileSize(video.size);
|
const fileSize = this.formatFileSize(video.size);
|
||||||
|
|
||||||
|
// Generate or get thumbnail
|
||||||
|
const thumbnailSrc = this.getThumbnailSrc(video);
|
||||||
|
|
||||||
if (this.currentView === 'grid') {
|
if (this.currentView === 'grid') {
|
||||||
return `
|
return `
|
||||||
<div class="video-card" data-video-path="${video.path}">
|
<div class="video-card" data-video-path="${video.path}">
|
||||||
<div class="video-thumbnail">
|
<div class="video-thumbnail" data-video-path="${video.path}">
|
||||||
${video.thumbnail ?
|
${thumbnailSrc ?
|
||||||
`<img src="${video.thumbnail}" alt="${video.name}">` :
|
`<img src="${thumbnailSrc}" alt="${video.name}" onload="this.style.opacity=1" style="opacity:0; transition: opacity 0.3s;">` :
|
||||||
'🎬'
|
`<div class="thumbnail-placeholder" data-video-path="${video.path}">🎬</div>`
|
||||||
}
|
}
|
||||||
<div class="video-duration">${duration}</div>
|
<div class="video-duration">${duration}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,10 +296,10 @@ class VideoLibrary {
|
||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
<div class="video-list-item" data-video-path="${video.path}">
|
<div class="video-list-item" data-video-path="${video.path}">
|
||||||
<div class="list-thumbnail">
|
<div class="list-thumbnail" data-video-path="${video.path}">
|
||||||
${video.thumbnail ?
|
${thumbnailSrc ?
|
||||||
`<img src="${video.thumbnail}" alt="${video.name}" class="list-thumbnail">` :
|
`<img src="${thumbnailSrc}" alt="${video.name}" class="list-thumbnail" onload="this.style.opacity=1" style="opacity:0; transition: opacity 0.3s;">` :
|
||||||
`<div class="list-thumbnail" style="display: flex; align-items: center; justify-content: center; font-size: 1.5rem;">🎬</div>`
|
`<div class="thumbnail-placeholder list-thumbnail" data-video-path="${video.path}" style="display: flex; align-items: center; justify-content: center; font-size: 1.5rem;">🎬</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-details">
|
<div class="list-details">
|
||||||
|
|
@ -310,10 +342,10 @@ class VideoLibrary {
|
||||||
// Add to playlist button events
|
// Add to playlist button events
|
||||||
const playlistButtons = this.libraryContent.querySelectorAll('.add-to-playlist');
|
const playlistButtons = this.libraryContent.querySelectorAll('.add-to-playlist');
|
||||||
playlistButtons.forEach(button => {
|
playlistButtons.forEach(button => {
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const videoPath = button.closest('[data-video-path]').dataset.videoPath;
|
const videoPath = button.closest('[data-video-path]').dataset.videoPath;
|
||||||
this.addToPlaylist(videoPath);
|
await this.addToPlaylist(videoPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -347,10 +379,10 @@ class VideoLibrary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addToPlaylist(videoPath) {
|
async addToPlaylist(videoPath) {
|
||||||
const video = this.videos.find(video => video.path === videoPath);
|
const video = this.videos.find(video => video.path === videoPath);
|
||||||
if (video && this.pornCinema) {
|
if (video && this.pornCinema) {
|
||||||
this.pornCinema.addToPlaylist(video);
|
await this.pornCinema.addToPlaylist(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,4 +474,344 @@ class VideoLibrary {
|
||||||
getFilteredVideos() {
|
getFilteredVideos() {
|
||||||
return this.filteredVideos;
|
return this.filteredVideos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadStoredVideos() {
|
||||||
|
console.log('📁 Loading stored videos from localStorage...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fallback to localStorage
|
||||||
|
const storedVideos = JSON.parse(localStorage.getItem('videoFiles') || '{}');
|
||||||
|
console.log('📁 Falling back to stored videos');
|
||||||
|
|
||||||
|
const allVideos = Object.values(storedVideos).flat();
|
||||||
|
|
||||||
|
if (!allVideos || allVideos.length === 0) {
|
||||||
|
console.warn('📁 No videos found in storage either');
|
||||||
|
this.displayEmptyLibrary('No videos found. Please add video files to your video directories.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store videos with enriched metadata
|
||||||
|
this.videos = allVideos.map(video => ({
|
||||||
|
...video,
|
||||||
|
category: video.category || 'unknown',
|
||||||
|
qualities: this.detectVideoQualities(video),
|
||||||
|
addedDate: video.addedDate || new Date().toISOString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`📁 Loaded ${this.videos.length} videos`);
|
||||||
|
console.log('📁 Sample video object:', this.videos[0]);
|
||||||
|
|
||||||
|
// Apply current filters and display
|
||||||
|
this.applyFiltersAndSort();
|
||||||
|
this.displayLibrary();
|
||||||
|
|
||||||
|
// Load durations for videos that don't have them
|
||||||
|
this.loadMissingDurations();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading stored videos:', error);
|
||||||
|
this.displayEmptyLibrary('Error loading video library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnailSrc(video) {
|
||||||
|
// Check if video has existing thumbnail
|
||||||
|
if (video.thumbnail) {
|
||||||
|
// Convert thumbnail path for Electron if needed
|
||||||
|
if (window.electronAPI && video.thumbnail.match(/^[A-Za-z]:\\/)) {
|
||||||
|
return `file:///${video.thumbnail.replace(/\\/g, '/')}`;
|
||||||
|
}
|
||||||
|
return video.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a cached thumbnail
|
||||||
|
const cacheKey = `thumbnail_${video.path}`;
|
||||||
|
const cacheVersionKey = `thumbnail_version_${video.path}`;
|
||||||
|
const currentVersion = '2.0'; // Updated version for aspect ratio support
|
||||||
|
|
||||||
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
|
const cachedVersion = localStorage.getItem(cacheVersionKey);
|
||||||
|
|
||||||
|
if (cachedData && cachedVersion === currentVersion) {
|
||||||
|
try {
|
||||||
|
const thumbnailData = JSON.parse(cachedData);
|
||||||
|
return thumbnailData.dataUrl || cachedData; // Support both new and old format
|
||||||
|
} catch (e) {
|
||||||
|
// Old format - just a data URL string
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
} else if (cachedData) {
|
||||||
|
// Clear old cache
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
localStorage.removeItem(cacheVersionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate thumbnail asynchronously
|
||||||
|
this.generateThumbnail(video);
|
||||||
|
|
||||||
|
return null; // Will show placeholder initially
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateThumbnail(video) {
|
||||||
|
try {
|
||||||
|
// Convert video path for Electron if needed
|
||||||
|
let videoSrc = video.path;
|
||||||
|
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
||||||
|
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary video element
|
||||||
|
const tempVideo = document.createElement('video');
|
||||||
|
tempVideo.crossOrigin = 'anonymous';
|
||||||
|
tempVideo.preload = 'metadata';
|
||||||
|
tempVideo.style.display = 'none';
|
||||||
|
document.body.appendChild(tempVideo);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
tempVideo.addEventListener('loadedmetadata', () => {
|
||||||
|
// Seek to 10% of video duration for thumbnail
|
||||||
|
tempVideo.currentTime = Math.min(tempVideo.duration * 0.1, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
tempVideo.addEventListener('seeked', () => {
|
||||||
|
try {
|
||||||
|
// Create canvas to capture frame
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Get video dimensions
|
||||||
|
const videoWidth = tempVideo.videoWidth;
|
||||||
|
const videoHeight = tempVideo.videoHeight;
|
||||||
|
const aspectRatio = videoWidth / videoHeight;
|
||||||
|
|
||||||
|
// Set canvas size based on aspect ratio
|
||||||
|
const maxWidth = 320;
|
||||||
|
const maxHeight = 240; // Increased max height for portrait videos
|
||||||
|
|
||||||
|
let canvasWidth, canvasHeight;
|
||||||
|
|
||||||
|
if (aspectRatio >= 1) {
|
||||||
|
// Landscape or square video
|
||||||
|
canvasWidth = Math.min(maxWidth, videoWidth);
|
||||||
|
canvasHeight = canvasWidth / aspectRatio;
|
||||||
|
} else {
|
||||||
|
// Portrait video
|
||||||
|
canvasHeight = Math.min(maxHeight, videoHeight);
|
||||||
|
canvasWidth = canvasHeight * aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = canvasWidth;
|
||||||
|
canvas.height = canvasHeight;
|
||||||
|
|
||||||
|
// Draw video frame to canvas maintaining aspect ratio
|
||||||
|
ctx.drawImage(tempVideo, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
// Convert to base64 data URL
|
||||||
|
const thumbnailDataUrl = canvas.toDataURL('image/jpeg', 0.7);
|
||||||
|
|
||||||
|
// Cache the thumbnail with metadata
|
||||||
|
const cacheKey = `thumbnail_${video.path}`;
|
||||||
|
const cacheVersionKey = `thumbnail_version_${video.path}`;
|
||||||
|
const thumbnailData = {
|
||||||
|
dataUrl: thumbnailDataUrl,
|
||||||
|
width: canvasWidth,
|
||||||
|
height: canvasHeight,
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
isPortrait: aspectRatio < 1
|
||||||
|
};
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(thumbnailData));
|
||||||
|
localStorage.setItem(cacheVersionKey, '2.0');
|
||||||
|
|
||||||
|
// Update the display
|
||||||
|
this.updateThumbnailDisplay(video.path, thumbnailData);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(tempVideo);
|
||||||
|
resolve(thumbnailDataUrl);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
document.body.removeChild(tempVideo);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tempVideo.addEventListener('error', (e) => {
|
||||||
|
document.body.removeChild(tempVideo);
|
||||||
|
reject(new Error(`Failed to load video for thumbnail: ${e.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set timeout to avoid hanging
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tempVideo.parentNode) {
|
||||||
|
document.body.removeChild(tempVideo);
|
||||||
|
}
|
||||||
|
reject(new Error('Timeout generating thumbnail'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
tempVideo.src = videoSrc;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to generate thumbnail for ${video.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateThumbnailDisplay(videoPath, thumbnailData) {
|
||||||
|
// Handle both old (string) and new (object) thumbnail data formats
|
||||||
|
const thumbnailSrc = typeof thumbnailData === 'string' ? thumbnailData : thumbnailData.dataUrl;
|
||||||
|
const isPortrait = typeof thumbnailData === 'object' ? thumbnailData.isPortrait : false;
|
||||||
|
|
||||||
|
// Find all thumbnail elements for this video
|
||||||
|
const thumbnailElements = document.querySelectorAll(`[data-video-path="${videoPath}"]`);
|
||||||
|
|
||||||
|
thumbnailElements.forEach(element => {
|
||||||
|
if (element.classList.contains('video-thumbnail') || element.classList.contains('list-thumbnail')) {
|
||||||
|
// Replace placeholder with actual thumbnail
|
||||||
|
const placeholderDiv = element.querySelector('.thumbnail-placeholder');
|
||||||
|
if (placeholderDiv) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = thumbnailSrc;
|
||||||
|
img.alt = 'Video thumbnail';
|
||||||
|
img.style.opacity = '0';
|
||||||
|
img.style.transition = 'opacity 0.3s';
|
||||||
|
|
||||||
|
// Apply aspect ratio specific styling
|
||||||
|
if (isPortrait) {
|
||||||
|
img.style.width = 'auto';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.style.maxWidth = '100%';
|
||||||
|
} else {
|
||||||
|
img.style.width = '100%';
|
||||||
|
img.style.height = '100%';
|
||||||
|
img.style.objectFit = 'cover';
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
img.style.opacity = '1';
|
||||||
|
placeholderDiv.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (element.classList.contains('list-thumbnail')) {
|
||||||
|
img.className = 'list-thumbnail';
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMissingDurations() {
|
||||||
|
// Find videos without duration
|
||||||
|
const videosNeedingDuration = this.videos.filter(video => !video.duration || video.duration === 0);
|
||||||
|
|
||||||
|
if (videosNeedingDuration.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⏱️ Loading durations for ${videosNeedingDuration.length} videos...`);
|
||||||
|
|
||||||
|
// Load durations in batches to avoid overwhelming the system
|
||||||
|
const batchSize = 3;
|
||||||
|
for (let i = 0; i < videosNeedingDuration.length; i += batchSize) {
|
||||||
|
const batch = videosNeedingDuration.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
await Promise.all(batch.map(async (video) => {
|
||||||
|
try {
|
||||||
|
const duration = await this.getVideoDuration(video);
|
||||||
|
video.duration = duration;
|
||||||
|
|
||||||
|
// Update the display for this specific video
|
||||||
|
this.updateVideoDurationDisplay(video);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load duration for ${video.name}:`, error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Small delay between batches
|
||||||
|
if (i + batchSize < videosNeedingDuration.length) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVideoDuration(video) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Create a temporary video element to load metadata
|
||||||
|
const tempVideo = document.createElement('video');
|
||||||
|
tempVideo.preload = 'metadata';
|
||||||
|
|
||||||
|
// Convert path to proper format for Electron
|
||||||
|
let videoSrc = video.path;
|
||||||
|
if (window.electronAPI && video.path.match(/^[A-Za-z]:\\/)) {
|
||||||
|
// Absolute Windows path - convert to file:// URL
|
||||||
|
videoSrc = `file:///${video.path.replace(/\\/g, '/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempVideo.addEventListener('loadedmetadata', () => {
|
||||||
|
const duration = tempVideo.duration;
|
||||||
|
tempVideo.remove(); // Clean up
|
||||||
|
resolve(duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
tempVideo.addEventListener('error', (e) => {
|
||||||
|
tempVideo.remove(); // Clean up
|
||||||
|
reject(new Error(`Failed to load video metadata: ${e.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set timeout to avoid hanging
|
||||||
|
setTimeout(() => {
|
||||||
|
tempVideo.remove();
|
||||||
|
reject(new Error('Timeout loading video metadata'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
tempVideo.src = videoSrc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVideoDurationDisplay(video) {
|
||||||
|
// Escape special characters in the path for CSS selector
|
||||||
|
const escapedPath = video.path.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
// Find video cards/items for this video and update the duration display
|
||||||
|
const videoElements = document.querySelectorAll(`[data-video-path="${escapedPath}"]`);
|
||||||
|
|
||||||
|
// If CSS selector fails, try a different approach
|
||||||
|
if (videoElements.length === 0) {
|
||||||
|
const allVideoElements = document.querySelectorAll('[data-video-path]');
|
||||||
|
const matchingElements = Array.from(allVideoElements).filter(el =>
|
||||||
|
el.getAttribute('data-video-path') === video.path
|
||||||
|
);
|
||||||
|
|
||||||
|
matchingElements.forEach((element) => {
|
||||||
|
this.updateSingleElementDuration(element, video);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
videoElements.forEach((element) => {
|
||||||
|
this.updateSingleElementDuration(element, video);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSingleElementDuration(element, video) {
|
||||||
|
// Check if this is a grid view card or list view item
|
||||||
|
if (element.classList.contains('video-card')) {
|
||||||
|
// Grid view - duration is in .video-duration
|
||||||
|
const durationElement = element.querySelector('.video-duration');
|
||||||
|
if (durationElement) {
|
||||||
|
const formattedDuration = this.formatDuration(video.duration);
|
||||||
|
durationElement.textContent = formattedDuration;
|
||||||
|
}
|
||||||
|
} else if (element.classList.contains('video-list-item')) {
|
||||||
|
// List view - duration is in the second .list-meta element
|
||||||
|
const metaElements = element.querySelectorAll('.list-meta');
|
||||||
|
if (metaElements.length >= 2) {
|
||||||
|
const formattedDuration = this.formatDuration(video.duration);
|
||||||
|
metaElements[1].textContent = formattedDuration; // Second meta is duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -562,6 +562,11 @@
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Portrait video thumbnails - use contain to show full image */
|
||||||
|
.video-thumbnail img[style*="object-fit: contain"] {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
.video-duration {
|
.video-duration {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue