Add app auth, dashboard, scheduler, video management, and new scrapers
- JWT-based app authentication with user roles, folder/route access control - Dashboard with storage stats, health checks, and recent activity - Auto-download/scrape scheduler (12h interval) with per-user and per-job configs - Video upload, tagging, HLS transcoding, and detail pages - New scrapers: LeakGallery, Mega (megajs), yt-dlp - FlareSolverr integration for Cloudflare-protected sites - Gallery: advanced filtering (date, size, search), sort modes, equal-mix shuffle - Forum sites management with stored cookies/auth - GridWall/GridCell components for responsive media grid - Media API with folder-access permissions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { Router } from 'express';
|
||||
import { getMediaFiles, getSetting, getUserFolderAccess } from './db.js';
|
||||
|
||||
const router = Router();
|
||||
const MEDIA_PATH = process.env.MEDIA_PATH || './data/media';
|
||||
|
||||
// GET /api/media?users=folder1,folder2&type=video,image
|
||||
router.get('/api/media', (req, res) => {
|
||||
const { users, type } = req.query;
|
||||
|
||||
if (!users) return res.status(400).json({ error: 'users parameter is required' });
|
||||
if (!type) return res.status(400).json({ error: 'type parameter is required' });
|
||||
|
||||
const folders = users.split(',').map(u => u.trim()).filter(Boolean);
|
||||
const types = type.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
|
||||
|
||||
if (folders.length === 0) return res.status(400).json({ error: 'at least one user/folder is required' });
|
||||
|
||||
const validTypes = ['video', 'image'];
|
||||
const invalid = types.find(t => !validTypes.includes(t));
|
||||
if (invalid) return res.status(400).json({ error: `invalid type: ${invalid}. Must be video, image, or both` });
|
||||
|
||||
// Enforce folder access for non-admin users
|
||||
if (req.user && req.user.role !== 'admin') {
|
||||
const allowed = getUserFolderAccess(req.user.id);
|
||||
if (allowed.length > 0) {
|
||||
const denied = folders.filter(f => !allowed.includes(f));
|
||||
if (denied.length > 0) {
|
||||
return res.status(403).json({ error: `access denied to folders: ${denied.join(', ')}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If both types requested, query all; otherwise query the single type
|
||||
const typeFilter = types.length === 2 ? 'all' : types[0];
|
||||
|
||||
const { rows } = getMediaFiles({
|
||||
folders,
|
||||
type: typeFilter,
|
||||
offset: 0,
|
||||
limit: 999999999,
|
||||
});
|
||||
|
||||
const hlsEnabled = getSetting('hls_enabled') === 'true' || process.env.HLS_ENABLED === 'true';
|
||||
|
||||
const results = rows.map(r => {
|
||||
const item = {
|
||||
folder: r.folder,
|
||||
path: `${MEDIA_PATH}/${r.folder}/${r.filename}`,
|
||||
type: r.type,
|
||||
};
|
||||
if (r.type === 'video' && hlsEnabled) {
|
||||
item.hlsUrl = `/api/hls/${encodeURIComponent(r.folder)}/${encodeURIComponent(r.filename)}/master.m3u8`;
|
||||
} else if (r.type === 'video') {
|
||||
item.mediaUrl = `/api/gallery/media/${encodeURIComponent(r.folder)}/${encodeURIComponent(r.filename)}`;
|
||||
}
|
||||
if (r.type === 'image') {
|
||||
item.mediaUrl = `/api/gallery/media/${encodeURIComponent(r.folder)}/${encodeURIComponent(r.filename)}`;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user