nextav/src/lib/scanner.ts

136 lines
4.9 KiB
TypeScript

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);
};