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 path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import ffmpeg from "fluent-ffmpeg";
|
import ffmpeg from "fluent-ffmpeg";
|
||||||
|
import { ThumbnailManager } from "./thumbnails";
|
||||||
|
|
||||||
const VIDEO_EXTENSIONS = ["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v"];
|
const VIDEO_EXTENSIONS = ["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v"];
|
||||||
const PHOTO_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"];
|
const PHOTO_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"];
|
||||||
|
|
@ -67,9 +68,9 @@ const scanLibrary = async (library: { id: number; path: string }) => {
|
||||||
const isText = TEXT_EXTENSIONS.some(t => t.toLowerCase() === cleanExt);
|
const isText = TEXT_EXTENSIONS.some(t => t.toLowerCase() === cleanExt);
|
||||||
|
|
||||||
const mediaType = isVideo ? "video" : isPhoto ? "photo" : "text";
|
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);
|
// Generate hashed thumbnail path
|
||||||
const thumbnailUrl = `/thumbnails/${thumbnailFileName}`;
|
const { folderPath, fullPath, url } = ThumbnailManager.getThumbnailPath(file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingMedia = db.prepare("SELECT * FROM media WHERE path = ?").get(file);
|
const existingMedia = db.prepare("SELECT * FROM media WHERE path = ?").get(file);
|
||||||
|
|
@ -77,27 +78,24 @@ const scanLibrary = async (library: { id: number; path: string }) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure thumbnails directory exists
|
// Ensure hashed directory structure exists
|
||||||
const thumbnailsDir = path.join(process.cwd(), "public", "thumbnails");
|
ThumbnailManager.ensureDirectory(folderPath);
|
||||||
if (!fs.existsSync(thumbnailsDir)) {
|
|
||||||
fs.mkdirSync(thumbnailsDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalThumbnailUrl = thumbnailUrl;
|
let finalThumbnailUrl = url;
|
||||||
let thumbnailGenerated = false;
|
let thumbnailGenerated = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
await generateVideoThumbnail(file, thumbnailPath);
|
await generateVideoThumbnail(file, fullPath);
|
||||||
thumbnailGenerated = true;
|
thumbnailGenerated = true;
|
||||||
} else if (isPhoto) {
|
} else if (isPhoto) {
|
||||||
await generatePhotoThumbnail(file, thumbnailPath);
|
await generatePhotoThumbnail(file, fullPath);
|
||||||
thumbnailGenerated = true;
|
thumbnailGenerated = true;
|
||||||
}
|
}
|
||||||
} catch (thumbnailError) {
|
} catch (thumbnailError) {
|
||||||
console.warn(`Thumbnail generation failed for ${file}:`, thumbnailError);
|
console.warn(`Thumbnail generation failed for ${file}:`, thumbnailError);
|
||||||
// Use fallback thumbnail based on media type
|
// Use fallback thumbnail based on media type
|
||||||
finalThumbnailUrl = isVideo ? "/placeholder-video.svg" : isPhoto ? "/placeholder-photo.svg" : "/placeholder.svg";
|
finalThumbnailUrl = ThumbnailManager.getFallbackThumbnailUrl(mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = {
|
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