136 lines
4.9 KiB
TypeScript
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);
|
|
}; |