/** * Library Management System for The Academy * Handles media tagging, organization, quality ratings, and preference-based filtering */ class LibraryManager { constructor() { this.initializeLibrary(); } /** * Initialize library structure in gameData if it doesn't exist * Integrates with existing desktopFileManager video/photo system */ initializeLibrary() { if (!window.gameData.academyLibrary) { window.gameData.academyLibrary = { version: 1, lastUpdated: new Date().toISOString(), // Media items with tags and metadata // Keys are file paths from desktopFileManager.getAllVideos() media: { // Key: file path from existing library, Value: metadata object // Example: "C:/Users/drew/Videos/video1.mp4": { tags: [...], rating: 5, ... } }, // Tag definitions and usage counts tags: { // Content theme tags contentThemes: ['dominance', 'submission', 'humiliation', 'worship', 'edging', 'denial', 'cei', 'sissy', 'bbc', 'feet', 'femdom', 'maledom', 'lesbian', 'gay', 'trans', 'hentai', 'pov', 'joi', 'gooning', 'mindbreak'], // Visual style tags visualStyles: ['solo', 'couples', 'group', 'amateur', 'professional', 'animated', 'real', 'closeup', 'fullbody', 'pov', 'compilation', 'pmv', 'slowmo', 'artistic', 'raw'], // Intensity tags intensity: ['soft', 'moderate', 'intense', 'extreme', 'hardcore', 'gentle', 'rough'], // Audio tags audio: ['music', 'talking', 'moaning', 'silent', 'asmr', 'binaural', 'soundfx', 'voiceover', 'ambient'], // Quality tags quality: ['lowres', 'sd', 'hd', '720p', '1080p', '4k', 'vr'], // Duration tags duration: ['short', 'medium', 'long', 'loop', 'extended'], // Actor/performer tags (will be populated dynamically) performers: [], // Custom user tags custom: [] }, // Tag usage statistics tagStats: { // Tag name -> count of media items with that tag }, // Quality ratings by user ratings: { // File path -> rating (1-5 stars) }, // User's favorite/disliked items favorites: [], disliked: [], // Collection system (playlists/categories) collections: { // Collection name -> array of file paths }, // Scan history (track what's been tagged) scanHistory: { lastFullScan: null, scannedFolders: [], totalItemsScanned: 0, totalItemsTagged: 0 } }; this.saveLibrary(); } } /** * Get the full library object * @returns {Object} Library object */ getLibrary() { return window.gameData.academyLibrary; } /** * Add or update media item with tags and metadata * @param {string} filePath - Path to media file * @param {Object} metadata - Metadata object { tags, rating, duration, etc. } * @returns {boolean} Success status */ addMediaItem(filePath, metadata = {}) { const library = this.getLibrary(); // Create or update media item if (!library.media[filePath]) { library.media[filePath] = { filePath: filePath, tags: [], rating: 0, timesPlayed: 0, lastPlayed: null, addedDate: new Date().toISOString(), duration: null, fileSize: null, format: this.getFileFormat(filePath) }; } // Merge metadata Object.assign(library.media[filePath], metadata); // Update tag statistics if (metadata.tags) { this.updateTagStats(metadata.tags); } library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Add tags to a media item * @param {string} filePath - Path to media file * @param {Array} tags - Array of tag names * @returns {boolean} Success status */ addTags(filePath, tags) { const library = this.getLibrary(); if (!library.media[filePath]) { this.addMediaItem(filePath); } const item = library.media[filePath]; // Add new tags (avoid duplicates) tags.forEach(tag => { if (!item.tags.includes(tag)) { item.tags.push(tag); } }); // Update tag statistics this.updateTagStats(tags); library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Remove tags from a media item * @param {string} filePath - Path to media file * @param {Array} tags - Array of tag names to remove * @returns {boolean} Success status */ removeTags(filePath, tags) { const library = this.getLibrary(); if (!library.media[filePath]) { return false; } const item = library.media[filePath]; tags.forEach(tag => { const index = item.tags.indexOf(tag); if (index > -1) { item.tags.splice(index, 1); // Decrement tag stat if (library.tagStats[tag]) { library.tagStats[tag] = Math.max(0, library.tagStats[tag] - 1); } } }); library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Bulk tag multiple files at once * @param {Array} filePaths - Array of file paths * @param {Array} tags - Array of tags to add to all files * @returns {Object} Result object with success count */ bulkAddTags(filePaths, tags) { let successCount = 0; filePaths.forEach(filePath => { if (this.addTags(filePath, tags)) { successCount++; } }); return { success: true, filesTagged: successCount, totalFiles: filePaths.length, tags: tags }; } /** * Set quality rating for a media item * @param {string} filePath - Path to media file * @param {number} rating - Rating (1-5) * @returns {boolean} Success status */ setRating(filePath, rating) { if (rating < 1 || rating > 5) { console.error('Rating must be between 1 and 5'); return false; } const library = this.getLibrary(); if (!library.media[filePath]) { this.addMediaItem(filePath); } library.media[filePath].rating = rating; library.ratings[filePath] = rating; library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Add media item to favorites * @param {string} filePath - Path to media file * @returns {boolean} Success status */ addToFavorites(filePath) { const library = this.getLibrary(); if (!library.favorites.includes(filePath)) { library.favorites.push(filePath); // Remove from disliked if present const dislikedIndex = library.disliked.indexOf(filePath); if (dislikedIndex > -1) { library.disliked.splice(dislikedIndex, 1); } library.lastUpdated = new Date().toISOString(); this.saveLibrary(); } return true; } /** * Add media item to disliked list * @param {string} filePath - Path to media file * @returns {boolean} Success status */ addToDisliked(filePath) { const library = this.getLibrary(); if (!library.disliked.includes(filePath)) { library.disliked.push(filePath); // Remove from favorites if present const favIndex = library.favorites.indexOf(filePath); if (favIndex > -1) { library.favorites.splice(favIndex, 1); } library.lastUpdated = new Date().toISOString(); this.saveLibrary(); } return true; } /** * Search/filter media by tags and criteria * @param {Object} filters - Filter configuration * @returns {Array} Array of matching media items */ searchMedia(filters = {}) { const library = this.getLibrary(); let results = Object.values(library.media); // Filter by required tags (item must have ALL these tags) if (filters.requiredTags && filters.requiredTags.length > 0) { results = results.filter(item => filters.requiredTags.every(tag => item.tags.includes(tag)) ); } // Filter by any tags (item must have AT LEAST ONE of these tags) if (filters.anyTags && filters.anyTags.length > 0) { results = results.filter(item => filters.anyTags.some(tag => item.tags.includes(tag)) ); } // Filter by excluded tags (item must NOT have any of these tags) if (filters.excludedTags && filters.excludedTags.length > 0) { results = results.filter(item => !filters.excludedTags.some(tag => item.tags.includes(tag)) ); } // Filter by minimum rating if (filters.minRating) { results = results.filter(item => item.rating >= filters.minRating); } // Filter by favorites only if (filters.favoritesOnly) { results = results.filter(item => library.favorites.includes(item.filePath)); } // Exclude disliked if (filters.excludeDisliked) { results = results.filter(item => !library.disliked.includes(item.filePath)); } // Filter by format if (filters.format) { results = results.filter(item => item.format === filters.format); } // Sort results if (filters.sortBy) { results = this.sortResults(results, filters.sortBy, filters.sortOrder || 'desc'); } // Limit results if (filters.limit) { results = results.slice(0, filters.limit); } return results; } /** * Get media items matching user's preferences (from preferenceManager) * @param {Object} preferenceFilter - Filter from preferenceManager.getContentFilter() * @param {number} limit - Maximum number of items to return * @returns {Array} Array of matching media items */ getMediaForPreferences(preferenceFilter, limit = 10) { const filters = { anyTags: [ ...preferenceFilter.themes, ...preferenceFilter.visuals, ...preferenceFilter.audio, ...preferenceFilter.tones ], excludedTags: [ ...preferenceFilter.exclude.hardLimits, ...preferenceFilter.exclude.softLimits ], excludeDisliked: true, sortBy: 'rating', sortOrder: 'desc', limit: limit }; return this.searchMedia(filters); } /** * Create a collection (playlist/category) * @param {string} name - Collection name * @param {Array} filePaths - Array of file paths * @returns {boolean} Success status */ createCollection(name, filePaths = []) { const library = this.getLibrary(); library.collections[name] = filePaths; library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Add items to a collection * @param {string} collectionName - Collection name * @param {Array} filePaths - File paths to add * @returns {boolean} Success status */ addToCollection(collectionName, filePaths) { const library = this.getLibrary(); if (!library.collections[collectionName]) { this.createCollection(collectionName, filePaths); } else { filePaths.forEach(filePath => { if (!library.collections[collectionName].includes(filePath)) { library.collections[collectionName].push(filePath); } }); } library.lastUpdated = new Date().toISOString(); this.saveLibrary(); return true; } /** * Get all items in a collection * @param {string} collectionName - Collection name * @returns {Array} Array of media items */ getCollection(collectionName) { const library = this.getLibrary(); if (!library.collections[collectionName]) { return []; } return library.collections[collectionName].map(filePath => library.media[filePath] ).filter(item => item !== undefined); } /** * Get library statistics * @returns {Object} Stats object */ getLibraryStats() { const library = this.getLibrary(); const totalItems = Object.keys(library.media).length; const taggedItems = Object.values(library.media).filter(item => item.tags.length > 0).length; const ratedItems = Object.values(library.media).filter(item => item.rating > 0).length; // Calculate average rating const ratings = Object.values(library.media).map(item => item.rating).filter(r => r > 0); const avgRating = ratings.length > 0 ? (ratings.reduce((a, b) => a + b, 0) / ratings.length).toFixed(2) : 0; return { totalItems: totalItems, taggedItems: taggedItems, untaggedItems: totalItems - taggedItems, ratedItems: ratedItems, averageRating: parseFloat(avgRating), totalFavorites: library.favorites.length, totalDisliked: library.disliked.length, totalCollections: Object.keys(library.collections).length, totalTags: Object.keys(library.tagStats).length, mostUsedTags: this.getMostUsedTags(5), lastUpdated: library.lastUpdated, scanHistory: library.scanHistory }; } /** * Get most used tags * @param {number} limit - Number of tags to return * @returns {Array} Array of {tag, count} objects */ getMostUsedTags(limit = 10) { const library = this.getLibrary(); return Object.entries(library.tagStats) .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(([tag, count]) => ({ tag, count })); } /** * Get all available tags by category * @returns {Object} Tags organized by category */ getAllTags() { return this.getLibrary().tags; } /** * Sync with existing video library from desktopFileManager * Imports videos from existing library and preserves existing Academy tags/ratings */ syncWithExistingLibrary() { console.log('🔄 Syncing with existing video library...'); let allVideos = []; // Get videos from desktopFileManager (primary source) if (window.desktopFileManager && typeof window.desktopFileManager.getAllVideos === 'function') { allVideos = window.desktopFileManager.getAllVideos(); console.log(`📹 Found ${allVideos.length} videos from desktopFileManager`); } // Fallback to unifiedVideoLibrary in localStorage if (allVideos.length === 0) { const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}'); allVideos = unifiedData.allVideos || []; console.log(`📹 Found ${allVideos.length} videos from unifiedVideoLibrary`); } // Import videos into Academy library while preserving existing Academy metadata const library = this.getLibrary(); let newCount = 0; let existingCount = 0; allVideos.forEach(video => { const filePath = video.path || video.name; if (!library.media[filePath]) { // New video - add with minimal metadata library.media[filePath] = { filePath: filePath, name: video.name, tags: [], rating: 0, timesPlayed: 0, lastPlayed: null, addedDate: new Date().toISOString(), duration: video.duration || null, fileSize: video.size || null, format: video.format || this.getFileFormat(filePath), // Preserve original library metadata originalSource: video.directory || video.source || 'Unknown', resolution: video.resolution || null, thumbnail: video.thumbnail || null }; newCount++; } else { // Existing video - update non-Academy fields only library.media[filePath].name = video.name; library.media[filePath].duration = video.duration || library.media[filePath].duration; library.media[filePath].fileSize = video.size || library.media[filePath].fileSize; library.media[filePath].format = video.format || library.media[filePath].format; library.media[filePath].resolution = video.resolution || library.media[filePath].resolution; library.media[filePath].thumbnail = video.thumbnail || library.media[filePath].thumbnail; existingCount++; } }); library.lastUpdated = new Date().toISOString(); library.scanHistory.lastFullScan = new Date().toISOString(); library.scanHistory.totalItemsScanned = allVideos.length; library.scanHistory.totalItemsTagged = Object.values(library.media).filter(item => item.tags.length > 0).length; this.saveLibrary(); console.log(`✅ Sync complete: ${newCount} new, ${existingCount} existing, ${allVideos.length} total videos`); return { total: allVideos.length, new: newCount, existing: existingCount }; } /** * Get all videos from existing library (convenience method) * @returns {Array} Array of video objects with Academy metadata */ getAllVideosWithMetadata() { const library = this.getLibrary(); return Object.values(library.media); } /** * Get video by file path * @param {string} filePath - Path to video file * @returns {Object|null} Video object with metadata */ getVideoByPath(filePath) { const library = this.getLibrary(); return library.media[filePath] || null; } /** * Add a custom tag * @param {string} tagName - Custom tag name * @returns {boolean} Success status */ addCustomTag(tagName) { const library = this.getLibrary(); if (!library.tags.custom.includes(tagName)) { library.tags.custom.push(tagName); library.lastUpdated = new Date().toISOString(); this.saveLibrary(); } return true; } /** * Record that a media item was played * @param {string} filePath - Path to media file * @returns {boolean} Success status */ recordPlay(filePath) { const library = this.getLibrary(); if (!library.media[filePath]) { this.addMediaItem(filePath); } library.media[filePath].timesPlayed = (library.media[filePath].timesPlayed || 0) + 1; library.media[filePath].lastPlayed = new Date().toISOString(); this.saveLibrary(); return true; } /** * Get recently played items * @param {number} limit - Number of items to return * @returns {Array} Array of media items */ getRecentlyPlayed(limit = 10) { const library = this.getLibrary(); return Object.values(library.media) .filter(item => item.lastPlayed !== null) .sort((a, b) => new Date(b.lastPlayed) - new Date(a.lastPlayed)) .slice(0, limit); } /** * Reset library to empty state * @returns {boolean} Success status */ resetLibrary() { delete window.gameData.academyLibrary; this.initializeLibrary(); return true; } /** * Add a directory of media files to the library * Uses desktopFileManager to select and scan a directory * @param {Array} suggestedTags - Optional tags to apply to all files in directory * @returns {Promise} Result object with file count and status */ async addDirectory(suggestedTags = []) { console.log('📁 Adding directory to library with suggested tags:', suggestedTags); // Check if desktopFileManager is available if (!window.desktopFileManager) { console.error('❌ desktopFileManager not available'); throw new Error('Desktop file manager is required to add directories'); } try { // Use desktopFileManager to add a video directory const result = await window.desktopFileManager.addVideoDirectory(); if (!result || !result.videoCount || result.videoCount === 0) { console.log('⚠️ No files added from directory'); return { success: false, fileCount: 0, message: 'No files selected or found' }; } // Get all videos from desktopFileManager after adding directory const allVideos = window.desktopFileManager.getAllVideos(); console.log(`📹 Total videos in library: ${allVideos.length}`); // Sync the library with the new videos this.syncWithExistingLibrary(); // If suggested tags were provided, apply them to the newly added videos if (suggestedTags.length > 0 && result.videos && result.videos.length > 0) { console.log(`🏷️ Applying suggested tags to ${result.videos.length} files:`, suggestedTags); // Get file paths from the result const filePaths = result.videos.map(video => video.path || video.fullPath || video.name); // Bulk add tags to all files const tagResult = this.bulkAddTags(filePaths, suggestedTags); console.log(`✅ Tagged ${tagResult.filesTagged} files with tags:`, suggestedTags); } const library = this.getLibrary(); library.scanHistory.lastFullScan = new Date().toISOString(); if (!library.scanHistory.scannedFolders.includes('user-selected')) { library.scanHistory.scannedFolders.push('user-selected'); } this.saveLibrary(); console.log(`✅ Directory added successfully: ${result.videoCount} files`); return { success: true, fileCount: result.videoCount, totalFiles: allVideos.length, message: `Added ${result.videoCount} files to library` }; } catch (error) { console.error('❌ Error adding directory:', error); throw error; } } // Helper methods /** * Update tag statistics * @param {Array} tags - Tags to increment */ updateTagStats(tags) { const library = this.getLibrary(); tags.forEach(tag => { library.tagStats[tag] = (library.tagStats[tag] || 0) + 1; }); } /** * Get file format from path * @param {string} filePath - File path * @returns {string} File format (mp4, webm, etc.) */ getFileFormat(filePath) { const ext = filePath.split('.').pop().toLowerCase(); return ext; } /** * Sort results by field * @param {Array} results - Results to sort * @param {string} field - Field to sort by * @param {string} order - 'asc' or 'desc' * @returns {Array} Sorted results */ sortResults(results, field, order = 'desc') { return results.sort((a, b) => { if (field === 'rating' || field === 'timesPlayed') { return order === 'desc' ? b[field] - a[field] : a[field] - b[field]; } else if (field === 'lastPlayed' || field === 'addedDate') { const dateA = new Date(a[field]); const dateB = new Date(b[field]); return order === 'desc' ? dateB - dateA : dateA - dateB; } else { return 0; } }); } /** * Save library to localStorage */ saveLibrary() { try { console.log('💾 libraryManager.saveLibrary() - completedLevels:', window.gameData.academyProgress?.completedLevels); console.trace('Stack trace:'); localStorage.setItem('webGame-data', JSON.stringify(window.gameData)); } catch (error) { console.error('Failed to save library:', error); } } } // Create global instance window.libraryManager = new LibraryManager(); /** * Load linked images from desktop file manager * Scans all linked image directories and populates window.allLinkedImages */ async function loadLinkedImages() { const linkedDirs = JSON.parse(localStorage.getItem('linkedImageDirectories') || '[]'); let allImages = []; console.log(`📁 Loading images from ${linkedDirs.length} linked directories...`); console.log('📁 electronAPI available:', !!window.electronAPI); if (window.electronAPI && linkedDirs.length > 0) { const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp)$/i; for (const dir of linkedDirs) { console.log(`📁 Scanning directory: ${dir.path}`); try { if (window.electronAPI.readImageDirectoryRecursive) { const filesPromise = window.electronAPI.readImageDirectoryRecursive(dir.path); let files = []; if (filesPromise && typeof filesPromise.then === 'function') { files = await filesPromise; } else if (Array.isArray(filesPromise)) { files = filesPromise; } if (files && files.length > 0) { const imageFiles = files.filter(file => { const fileName = typeof file === 'object' ? file.name : file; return imageExtensions.test(fileName); }); if (imageFiles.length > 0) { const dirImages = imageFiles.map(file => { if (typeof file === 'object' && file.name && file.path) { return file.path; } else { const fileName = typeof file === 'object' ? file.name : file; const fullPath = window.electronAPI.pathJoin ? window.electronAPI.pathJoin(dir.path, fileName) : `${dir.path}\\${fileName}`; return fullPath; } }); allImages = allImages.concat(dirImages); console.log(`📸 Added ${dirImages.length} images from ${dir.name}`); } } } } catch (error) { console.error(`Error scanning directory ${dir.path}:`, error); } } } console.log(`📸 Total images found: ${allImages.length}`); window.allLinkedImages = allImages; return allImages; } // Expose globally window.loadLinkedImages = loadLinkedImages;