refactor: remove thumbnail API and update thumbnail management
- Deleted the thumbnail API route to streamline the application structure. - Integrated ThumbnailManager for improved thumbnail path generation and directory management. - Updated media scanning logic to utilize hashed thumbnail paths and ensure directory existence. - Enhanced error handling for thumbnail generation with fallback options based on media type.
This commit is contained in:
parent
407c702e88
commit
f6a02d9328
BIN
data/media.db
BIN
data/media.db
Binary file not shown.
|
|
@ -0,0 +1,73 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ path: string[] }> }
|
||||
) {
|
||||
try {
|
||||
const { path: pathParts } = await params;
|
||||
|
||||
if (!pathParts || pathParts.length < 3) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid thumbnail path structure' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Reconstruct the full path
|
||||
// pathParts = ['ff', '01', 'ff0170402c7f75dbc1cd53128943832c_320.png']
|
||||
const relativePath = path.join(...pathParts);
|
||||
|
||||
// Construct the full file path
|
||||
const thumbnailPath = path.join(
|
||||
process.cwd(),
|
||||
'public',
|
||||
'thumbnails',
|
||||
relativePath
|
||||
);
|
||||
|
||||
// Validate file exists and is within thumbnails directory
|
||||
if (!thumbnailPath.startsWith(path.join(process.cwd(), 'public', 'thumbnails'))) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid path' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(thumbnailPath)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Thumbnail not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if it's actually a file (not directory)
|
||||
const stats = fs.statSync(thumbnailPath);
|
||||
if (!stats.isFile()) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Thumbnail not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Read the file
|
||||
const fileBuffer = fs.readFileSync(thumbnailPath);
|
||||
|
||||
// Return the image with appropriate headers
|
||||
return new NextResponse(fileBuffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error serving thumbnail:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ filename: string }> }
|
||||
) {
|
||||
try {
|
||||
const { filename } = await params;
|
||||
|
||||
// Construct the path to the thumbnail file
|
||||
const thumbnailPath = path.join(process.cwd(), 'public', 'thumbnails', filename);
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(thumbnailPath)) {
|
||||
return NextResponse.json({ error: 'Thumbnail not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Read the file
|
||||
const fileBuffer = fs.readFileSync(thumbnailPath);
|
||||
|
||||
// Return the image with appropriate headers
|
||||
return new NextResponse(fileBuffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error serving thumbnail:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { glob } from "glob";
|
|||
import path from "path";
|
||||
import fs from "fs";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import { ThumbnailManager } from "./thumbnails";
|
||||
|
||||
const VIDEO_EXTENSIONS = ["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v"];
|
||||
const PHOTO_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"];
|
||||
|
|
@ -67,37 +68,34 @@ const scanLibrary = async (library: { id: number; path: string }) => {
|
|||
const isText = TEXT_EXTENSIONS.some(t => t.toLowerCase() === cleanExt);
|
||||
|
||||
const mediaType = isVideo ? "video" : isPhoto ? "photo" : "text";
|
||||
const thumbnailFileName = `${path.parse(title).name}_${Date.now()}.png`;
|
||||
const thumbnailPath = path.join(process.cwd(), "public", "thumbnails", thumbnailFileName);
|
||||
const thumbnailUrl = `/thumbnails/${thumbnailFileName}`;
|
||||
|
||||
|
||||
// Generate hashed thumbnail path
|
||||
const { folderPath, fullPath, url } = ThumbnailManager.getThumbnailPath(file);
|
||||
|
||||
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 });
|
||||
}
|
||||
// Ensure hashed directory structure exists
|
||||
ThumbnailManager.ensureDirectory(folderPath);
|
||||
|
||||
let finalThumbnailUrl = thumbnailUrl;
|
||||
let finalThumbnailUrl = url;
|
||||
let thumbnailGenerated = false;
|
||||
|
||||
try {
|
||||
if (isVideo) {
|
||||
await generateVideoThumbnail(file, thumbnailPath);
|
||||
await generateVideoThumbnail(file, fullPath);
|
||||
thumbnailGenerated = true;
|
||||
} else if (isPhoto) {
|
||||
await generatePhotoThumbnail(file, thumbnailPath);
|
||||
await generatePhotoThumbnail(file, fullPath);
|
||||
thumbnailGenerated = true;
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
console.warn(`Thumbnail generation failed for ${file}:`, thumbnailError);
|
||||
// Use fallback thumbnail based on media type
|
||||
finalThumbnailUrl = isVideo ? "/placeholder-video.svg" : isPhoto ? "/placeholder-photo.svg" : "/placeholder.svg";
|
||||
finalThumbnailUrl = ThumbnailManager.getFallbackThumbnailUrl(mediaType);
|
||||
}
|
||||
|
||||
const media = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export class ThumbnailManager {
|
||||
private static readonly THUMBNAIL_WIDTH = 320;
|
||||
private static readonly HASH_ALGORITHM = 'md5';
|
||||
private static readonly THUMBNAIL_EXTENSION = '.png';
|
||||
|
||||
/**
|
||||
* Generate MD5 hash for a file path
|
||||
*/
|
||||
static generateHash(filePath: string): string {
|
||||
return crypto
|
||||
.createHash(this.HASH_ALGORITHM)
|
||||
.update(filePath)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate thumbnail filename based on hash and width
|
||||
*/
|
||||
static getThumbnailFilename(hash: string, width: number = this.THUMBNAIL_WIDTH): string {
|
||||
return `${hash}_${width}${this.THUMBNAIL_EXTENSION}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder structure for a given hash
|
||||
*/
|
||||
static getFolderStructure(hash: string): { folder1: string; folder2: string } {
|
||||
return {
|
||||
folder1: hash.substring(0, 2),
|
||||
folder2: hash.substring(2, 4)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete thumbnail path information
|
||||
*/
|
||||
static getThumbnailPath(filePath: string): {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
fullPath: string;
|
||||
url: string;
|
||||
} {
|
||||
const hash = this.generateHash(filePath);
|
||||
const { folder1, folder2 } = this.getFolderStructure(hash);
|
||||
const filename = this.getThumbnailFilename(hash);
|
||||
|
||||
const folderPath = path.join('thumbnails', folder1, folder2);
|
||||
const fullPath = path.join(process.cwd(), 'public', folderPath, filename);
|
||||
const url = `/${folderPath}/${filename}`;
|
||||
|
||||
return { folderPath, filename, fullPath, url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure thumbnail directory exists
|
||||
*/
|
||||
static ensureDirectory(folderPath: string): void {
|
||||
const fullPath = path.join(process.cwd(), 'public', folderPath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
fs.mkdirSync(fullPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base thumbnails directory
|
||||
*/
|
||||
static getThumbnailsBaseDir(): string {
|
||||
return path.join(process.cwd(), 'public', 'thumbnails');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a thumbnail exists
|
||||
*/
|
||||
static thumbnailExists(filePath: string): boolean {
|
||||
const { fullPath } = this.getThumbnailPath(filePath);
|
||||
return fs.existsSync(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fallback thumbnail URL based on media type
|
||||
*/
|
||||
static getFallbackThumbnailUrl(mediaType: 'video' | 'photo' | 'text'): string {
|
||||
switch (mediaType) {
|
||||
case 'video':
|
||||
return '/placeholder-video.svg';
|
||||
case 'photo':
|
||||
return '/placeholder-photo.svg';
|
||||
default:
|
||||
return '/placeholder.svg';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue