import { getDatabase } from "@/db"; import { glob } from "glob"; import path from "path"; import fs from "fs"; import ffmpeg from "fluent-ffmpeg"; const VIDEO_EXTENSIONS = ["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v"]; const PHOTO_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"]; const generateVideoThumbnail = (videoPath: string, thumbnailPath: string) => { return new Promise((resolve, reject) => { ffmpeg(videoPath) .on("end", () => resolve(thumbnailPath)) .on("error", (err) => reject(err)) .screenshots({ count: 1, folder: path.dirname(thumbnailPath), filename: path.basename(thumbnailPath), size: "320x?", // Maintain aspect ratio, width 320px timemarks: ["10%"] }); }); }; const generatePhotoThumbnail = (photoPath: string, thumbnailPath: string) => { return new Promise((resolve, reject) => { ffmpeg(photoPath) .on("end", () => resolve(thumbnailPath)) .on("error", (err) => reject(err)) .outputOptions(["-vf", "scale=320:240:force_original_aspect_ratio=decrease", "-quality", "85"]) .output(thumbnailPath) .run(); }); }; const scanLibrary = async (library: { id: number; path: string }) => { const db = getDatabase(); // Scan videos - handle all case variations const videoFiles = await glob(`${library.path}/**/*.*`, { nodir: true }); // Scan photos - handle all case variations const photoFiles = await glob(`${library.path}/**/*.*`, { nodir: true }); // Filter files by extension (case-insensitive) const filteredVideoFiles = videoFiles.filter(file => { const ext = path.extname(file).toLowerCase().replace('.', ''); return VIDEO_EXTENSIONS.includes(ext); }); const filteredPhotoFiles = photoFiles.filter(file => { const ext = path.extname(file).toLowerCase().replace('.', ''); return PHOTO_EXTENSIONS.includes(ext); }); const allFiles = [...filteredVideoFiles, ...filteredPhotoFiles]; for (const file of allFiles) { const stats = fs.statSync(file); const title = path.basename(file); const ext = path.extname(file).toLowerCase(); const cleanExt = ext.replace('.', '').toLowerCase(); const isVideo = VIDEO_EXTENSIONS.some(v => v.toLowerCase() === cleanExt); const isPhoto = PHOTO_EXTENSIONS.some(p => p.toLowerCase() === cleanExt); const mediaType = isVideo ? "video" : "photo"; const thumbnailFileName = `${path.parse(title).name}_${Date.now()}.png`; const thumbnailPath = path.join(process.cwd(), "public", "thumbnails", thumbnailFileName); const thumbnailUrl = `/thumbnails/${thumbnailFileName}`; try { const existingMedia = db.prepare("SELECT * FROM media WHERE path = ?").get(file); if (existingMedia) { continue; } // Ensure thumbnails directory exists const thumbnailsDir = path.join(process.cwd(), "public", "thumbnails"); if (!fs.existsSync(thumbnailsDir)) { fs.mkdirSync(thumbnailsDir, { recursive: true }); } let finalThumbnailUrl = thumbnailUrl; let thumbnailGenerated = false; try { if (isVideo) { await generateVideoThumbnail(file, thumbnailPath); thumbnailGenerated = true; } else if (isPhoto) { await generatePhotoThumbnail(file, thumbnailPath); thumbnailGenerated = true; } } catch (thumbnailError) { console.warn(`Thumbnail generation failed for ${file}:`, thumbnailError); // Use fallback thumbnail based on media type finalThumbnailUrl = isVideo ? "/placeholder-video.svg" : "/placeholder-photo.svg"; } const media = { library_id: library.id, path: file, type: mediaType, title: title, size: stats.size, thumbnail: finalThumbnailUrl, }; db.prepare( "INSERT INTO media (library_id, path, type, title, size, thumbnail) VALUES (?, ?, ?, ?, ?, ?)" ).run(media.library_id, media.path, media.type, media.title, media.size, media.thumbnail); console.log(`Successfully inserted ${mediaType}: ${title}${thumbnailGenerated ? ' with thumbnail' : ' with fallback thumbnail'}`); } catch (error: any) { if (error.code !== "SQLITE_CONSTRAINT_UNIQUE") { console.error(`Error inserting media: ${file}`, error); } } } }; export const scanAllLibraries = async () => { const db = getDatabase(); const libraries = db.prepare("SELECT * FROM libraries").all() as { id: number; path: string }[]; for (const library of libraries) { await scanLibrary(library); } }; export const scanSelectedLibrary = async (libraryId: number) => { const db = getDatabase(); const library = db.prepare("SELECT * FROM libraries WHERE id = ?").get(libraryId) as { id: number; path: string } | undefined; if (!library) { throw new Error(`Library with ID ${libraryId} not found`); } await scanLibrary(library); };