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:
dilgenfritz 2025-10-30 20:12:00 -05:00
parent c65a58bae9
commit a2af9ba8f3
4 changed files with 604 additions and 29 deletions

View File

@ -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

View File

@ -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';

View File

@ -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
}
}
}
} }

View File

@ -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;