mirror of
https://github.com/H4zeyaf/Breakallery.git
synced 2026-10-18 07:00:44 +02:00
173 lines
5.7 KiB
JavaScript
173 lines
5.7 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const mime = require('mime-types');
|
|
|
|
const { exec } = require('child_process');
|
|
const util = require('util');
|
|
const execPromise = util.promisify(exec);
|
|
|
|
const { THUMBNAILS_DIR, CAPTURES_DIR, VIDEOS_CACHE } = require('./paths');
|
|
|
|
class ThumbnailService {
|
|
constructor(ftpManager) {
|
|
this.ftp = ftpManager;
|
|
if (!fs.existsSync(THUMBNAILS_DIR)) {
|
|
fs.mkdirSync(THUMBNAILS_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
getCacheKey(remotePath) {
|
|
return crypto.createHash('md5').update(remotePath).digest('hex');
|
|
}
|
|
|
|
findLocalMediaFile(remotePath, fallbackPath) {
|
|
const filenames = [
|
|
remotePath ? path.basename(remotePath) : null,
|
|
fallbackPath ? path.basename(fallbackPath) : null,
|
|
].filter(Boolean);
|
|
|
|
if (fs.existsSync(CAPTURES_DIR)) {
|
|
for (const fname of filenames) {
|
|
const rootPath = path.join(CAPTURES_DIR, fname);
|
|
if (fs.existsSync(rootPath)) return rootPath;
|
|
|
|
try {
|
|
const entries = fs.readdirSync(CAPTURES_DIR, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
const subPath = path.join(CAPTURES_DIR, entry.name, fname);
|
|
if (fs.existsSync(subPath)) return subPath;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
// Check video cache
|
|
for (const p of [remotePath, fallbackPath]) {
|
|
if (p) {
|
|
const key = this.getCacheKey(p);
|
|
const ext = path.extname(p).toLowerCase();
|
|
const vPath = path.join(VIDEOS_CACHE, `${key}${ext}`);
|
|
if (fs.existsSync(vPath)) return vPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async serveThumbnail(req, res, remotePath, fallbackPath) {
|
|
if (!remotePath && !fallbackPath) {
|
|
return res.status(400).send('Path parameter is required');
|
|
}
|
|
|
|
const primaryKey = this.getCacheKey(remotePath || fallbackPath);
|
|
const cachedFilePath = path.join(THUMBNAILS_DIR, `${primaryKey}.thumb`);
|
|
|
|
// 1. Check local cache (primary and fallback)
|
|
if (fs.existsSync(cachedFilePath)) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return fs.createReadStream(cachedFilePath).pipe(res);
|
|
}
|
|
|
|
if (fallbackPath) {
|
|
const fallbackKey = this.getCacheKey(fallbackPath);
|
|
const fallbackFilePath = path.join(THUMBNAILS_DIR, `${fallbackKey}.thumb`);
|
|
if (fs.existsSync(fallbackFilePath)) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return fs.createReadStream(fallbackFilePath).pipe(res);
|
|
}
|
|
}
|
|
|
|
// 2. Check if local media file exists on disk
|
|
const localMedia = this.findLocalMediaFile(remotePath, fallbackPath);
|
|
if (localMedia) {
|
|
const ext = path.extname(localMedia).toLowerCase();
|
|
const isPhoto = ['.jpg', '.jpeg', '.png', '.webp'].includes(ext);
|
|
if (isPhoto) {
|
|
res.setHeader('Content-Type', mime.lookup(localMedia) || 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return fs.createReadStream(localMedia).pipe(res);
|
|
}
|
|
|
|
// Video: extract thumbnail frame
|
|
try {
|
|
await execPromise(`ffmpeg -y -ss 00:00:01 -i "${localMedia}" -frames:v 1 -update 1 -f image2 -vcodec mjpeg "${cachedFilePath}"`);
|
|
if (fs.existsSync(cachedFilePath)) {
|
|
res.setHeader('Content-Type', 'image/jpeg');
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return fs.createReadStream(cachedFilePath).pipe(res);
|
|
}
|
|
} catch (ffErr) {
|
|
console.warn('[Thumbnail] ffmpeg frame extraction warning:', ffErr.message);
|
|
}
|
|
}
|
|
|
|
// If console is offline and thumbnail is not in cache, return 404 immediately without hanging on FTP
|
|
if (!this.ftp.isOnline()) {
|
|
return res.status(404).send('Thumbnail not available offline');
|
|
}
|
|
|
|
// 2. Fetch from PS5 FTP (only if console is online)
|
|
try {
|
|
let buffer = null;
|
|
|
|
// Try designated thumbnail path first
|
|
if (remotePath) {
|
|
try {
|
|
buffer = await this.ftp.getBuffer(remotePath);
|
|
} catch (e) {
|
|
// Thumbnail path might not exist, proceed to fallback
|
|
}
|
|
}
|
|
|
|
// Try fallback full photo if thumbnail not present
|
|
if ((!buffer || buffer.length === 0) && fallbackPath) {
|
|
try {
|
|
buffer = await this.ftp.getBuffer(fallbackPath);
|
|
} catch (e) {
|
|
// Both failed
|
|
}
|
|
}
|
|
|
|
if (!buffer || buffer.length === 0) {
|
|
return res.status(404).send('Thumbnail not found');
|
|
}
|
|
|
|
// Cache to disk
|
|
try {
|
|
fs.writeFileSync(cachedFilePath, buffer);
|
|
} catch (err) {
|
|
console.error('[Thumbnail] Cache write error:', err.message);
|
|
}
|
|
|
|
const mimeType = mime.lookup(remotePath || fallbackPath) || 'image/jpeg';
|
|
res.setHeader('Content-Type', mimeType);
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return res.end(buffer);
|
|
} catch (err) {
|
|
console.error('[Thumbnail] Fetch error:', err.message);
|
|
return res.status(500).send('Error retrieving thumbnail');
|
|
}
|
|
}
|
|
|
|
serveCachedIcon(req, res, iconId) {
|
|
// iconId is like 'icon_PPSA01234'
|
|
const safeName = path.basename(iconId);
|
|
const iconPath = path.join(THUMBNAILS_DIR, `${safeName}.png`);
|
|
|
|
if (fs.existsSync(iconPath)) {
|
|
res.setHeader('Content-Type', 'image/png');
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
|
|
return fs.createReadStream(iconPath).pipe(res);
|
|
}
|
|
|
|
return res.status(404).send('Icon not found');
|
|
}
|
|
}
|
|
|
|
module.exports = ThumbnailService;
|