import { Router } from 'express'; import { readdirSync, statSync } from 'fs'; import { join, extname } from 'path'; import { getPostDateByFilename, getSetting } from './db.js'; const router = Router(); const MEDIA_PATH = process.env.MEDIA_PATH || './data/media'; const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']); const VIDEO_EXTS = new Set(['.mp4', '.mov', '.avi', '.webm', '.mkv', '.m4v']); function getMediaType(filename) { const ext = extname(filename).toLowerCase(); if (IMAGE_EXTS.has(ext)) return 'image'; if (VIDEO_EXTS.has(ext)) return 'video'; return null; } // GET /api/gallery/folders — list all folders with file counts router.get('/api/gallery/folders', (req, res, next) => { try { const entries = readdirSync(MEDIA_PATH, { withFileTypes: true }); const folders = []; for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue; const folderPath = join(MEDIA_PATH, entry.name); const files = readdirSync(folderPath).filter((f) => { return !f.startsWith('.') && getMediaType(f) !== null; }); if (files.length > 0) { const images = files.filter((f) => getMediaType(f) === 'image').length; const videos = files.filter((f) => getMediaType(f) === 'video').length; folders.push({ name: entry.name, total: files.length, images, videos }); } } folders.sort((a, b) => a.name.localeCompare(b.name)); res.json(folders); } catch (err) { next(err); } }); // GET /api/gallery/files?folder=&type=&sort=&offset=&limit= router.get('/api/gallery/files', (req, res, next) => { try { const { folder, type, sort, offset, limit } = req.query; const typeFilter = type || 'all'; // all, image, video const sortMode = sort || 'latest'; // latest, shuffle const offsetNum = parseInt(offset || '0', 10); const limitNum = parseInt(limit || '50', 10); let allFiles = []; const foldersParam = req.query.folders; // comma-separated list const foldersToScan = folder ? [folder] : foldersParam ? foldersParam.split(',').map((f) => f.trim()).filter(Boolean) : readdirSync(MEDIA_PATH, { withFileTypes: true }) .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_')) .map((e) => e.name); for (const dir of foldersToScan) { const dirPath = join(MEDIA_PATH, dir); let files; try { files = readdirSync(dirPath); } catch { continue; } for (const file of files) { if (file.startsWith('.')) continue; const mediaType = getMediaType(file); if (!mediaType) continue; if (typeFilter !== 'all' && mediaType !== typeFilter) continue; const filePath = join(dirPath, file); const stat = statSync(filePath); const postedAt = getPostDateByFilename(file); const fileObj = { folder: dir, filename: file, type: mediaType, size: stat.size, modified: stat.mtimeMs, postedAt: postedAt || null, url: `/api/gallery/media/${encodeURIComponent(dir)}/${encodeURIComponent(file)}`, }; if ((getSetting('hls_enabled') || process.env.HLS_ENABLED) === 'true' && mediaType === 'video') { fileObj.hlsUrl = `/api/hls/${encodeURIComponent(dir)}/${encodeURIComponent(file)}/master.m3u8`; } allFiles.push(fileObj); } } // Sort if (sortMode === 'shuffle') { for (let i = allFiles.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [allFiles[i], allFiles[j]] = [allFiles[j], allFiles[i]]; } } else { allFiles.sort((a, b) => { const aTime = a.postedAt ? new Date(a.postedAt).getTime() : a.modified; const bTime = b.postedAt ? new Date(b.postedAt).getTime() : b.modified; return bTime - aTime; }); } const total = allFiles.length; const page = allFiles.slice(offsetNum, offsetNum + limitNum); res.json({ total, offset: offsetNum, limit: limitNum, files: page }); } catch (err) { next(err); } }); // GET /api/gallery/media/:folder/:filename — serve actual file router.get('/api/gallery/media/:folder/:filename', (req, res) => { const { folder, filename } = req.params; // Prevent path traversal if (folder.includes('..') || filename.includes('..')) { return res.status(400).json({ error: 'Invalid path' }); } const filePath = join(MEDIA_PATH, folder, filename); res.sendFile(filePath, { root: '/' }, (err) => { if (err && !res.headersSent) { res.status(404).json({ error: 'File not found' }); } }); }); export default router;