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:
Trey T
2026-04-16 07:48:10 -05:00
parent 4903b84aef
commit 236f36aae6
54 changed files with 9986 additions and 420 deletions
+274 -6
View File
@@ -1,6 +1,13 @@
async function request(url, options = {}) {
try {
const response = await fetch(url, options);
// Handle 401 — redirect to login (skip for auth endpoints)
if (response.status === 401 && !url.startsWith('/api/app-auth/')) {
window.location.href = '/login';
return { error: 'Authentication required' };
}
const data = await response.json();
if (!response.ok) {
@@ -78,6 +85,14 @@ export function startDownload(userId, limit, resume, username) {
});
}
export function downloadPost(userId, username, postId, media, postedAt) {
return request('/api/download/post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, username, postId, media, postedAt }),
});
}
export function getDownloadStatus(userId) {
return request(`/api/download/${userId}/status`);
}
@@ -110,8 +125,8 @@ export function updateSettings(settings) {
});
}
export function getGalleryFiles({ folder, folders, type, sort, offset, limit } = {}) {
const query = buildQuery({ folder, folders: folders ? folders.join(',') : undefined, type, sort, offset, limit });
export function getGalleryFiles({ folder, folders, type, sort, offset, limit, dateFrom, dateTo, minSize, maxSize, search } = {}) {
const query = buildQuery({ folder, folders: folders ? folders.join(',') : undefined, type, sort, offset, limit, dateFrom, dateTo, minSize, maxSize, search });
return request(`/api/gallery/files${query}`);
}
@@ -131,8 +146,8 @@ export function getThumbsStatus() {
return request('/api/gallery/generate-thumbs/status');
}
export function scanDuplicates() {
return request('/api/gallery/scan-duplicates', { method: 'POST' });
export function scanDuplicates(mode = 'everywhere') {
return request(`/api/gallery/scan-duplicates?mode=${mode}`, { method: 'POST' });
}
export function getDuplicateScanStatus() {
@@ -152,6 +167,18 @@ export function deleteMediaFile(folder, filename) {
return request(`/api/gallery/media/${encodeURIComponent(folder)}/${encodeURIComponent(filename)}`, { method: 'DELETE' });
}
export function getNewMediaCount() {
return request('/api/gallery/new-count');
}
export function markGallerySeen() {
return request('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gallery_last_seen: new Date().toISOString() }),
});
}
export function startForumScrape(config) {
return request('/api/scrape/forum', {
method: 'POST',
@@ -176,6 +203,30 @@ export function startMediaLinkScrape(config) {
});
}
export function startMegaScrape(config) {
return request('/api/scrape/mega', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
}
export function startLeakGalleryScrape(config) {
return request('/api/scrape/leakgallery', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
}
export function startYtdlpScrape(config) {
return request('/api/scrape/ytdlp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
}
export function getScrapeJobs() {
return request('/api/scrape/jobs');
}
@@ -188,10 +239,227 @@ export function cancelScrapeJob(jobId) {
return request(`/api/scrape/jobs/${jobId}/cancel`, { method: 'POST' });
}
export function detectForumPages(url) {
export function detectForumPages(url, cookies) {
return request('/api/scrape/forum/detect-pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
body: JSON.stringify({ url, cookies }),
});
}
// --- FlareSolverr ---
export function getFlareSolverrStatus() {
return request('/api/flaresolverr/status');
}
export function refreshForumCookies(siteId) {
return request(`/api/flaresolverr/refresh/${siteId}`, { method: 'POST' });
}
// --- Forum Sites ---
export function getForumSites() {
return request('/api/scrape/forum-sites');
}
export function createForumSite(data) {
return request('/api/scrape/forum-sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
export function updateForumSite(id, data) {
return request(`/api/scrape/forum-sites/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
export function deleteForumSite(id) {
return request(`/api/scrape/forum-sites/${id}`, { method: 'DELETE' });
}
// --- Auto-download ---
export function getAutoDownloadUsers() {
return request('/api/download/auto');
}
export function addAutoDownloadUser(userId, username) {
return request(`/api/download/auto/${userId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username }),
});
}
export function removeAutoDownloadUser(userId) {
return request(`/api/download/auto/${userId}`, { method: 'DELETE' });
}
// --- Auto-scrape ---
export function getAutoScrapeJobs() {
return request('/api/scrape/auto');
}
export function addAutoScrapeJob(config) {
return request('/api/scrape/auto', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
});
}
export function removeAutoScrapeJob(id) {
return request(`/api/scrape/auto/${id}`, { method: 'DELETE' });
}
// --- Dashboard / Health ---
export function checkAuth() {
return request('/api/auth/check');
}
export function getDashboard() {
return request('/api/dashboard');
}
export function getHealth() {
return request('/api/health');
}
export function getActiveDownloadDetails() {
return request('/api/download/active/details');
}
// --- Videos ---
export function getVideos({ search, tags, minDuration, maxDuration, minWidth, sort, offset, limit } = {}) {
const query = buildQuery({
search, tags: tags ? tags.join(',') : undefined,
minDuration, maxDuration, minWidth, sort, offset, limit,
});
return request(`/api/videos${query}`);
}
export function getVideo(id) {
return request(`/api/videos/${id}`);
}
export function updateVideoMeta(id, data) {
return request(`/api/videos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
export function deleteVideo(id) {
return request(`/api/videos/${id}`, { method: 'DELETE' });
}
export function uploadVideo(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/videos/upload');
if (onProgress) {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(e.loaded / e.total);
};
}
xhr.onload = () => {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch {
reject(new Error('Invalid response'));
}
};
xhr.onerror = () => reject(new Error('Upload failed'));
const formData = new FormData();
formData.append('video', file);
xhr.send(formData);
});
}
export function scanVideos() {
return request('/api/videos/scan', { method: 'POST' });
}
export function getVideoScanStatus() {
return request('/api/videos/scan/status');
}
export function getVideoTags(search) {
const query = buildQuery({ search });
return request(`/api/videos/tags${query}`);
}
// --- App Auth ---
export function appAuthStatus() {
return request('/api/app-auth/status');
}
export function appAuthLogin(username, password) {
return request('/api/app-auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
}
export function appAuthLogout() {
return request('/api/app-auth/logout', { method: 'POST' });
}
export function appAuthMe() {
return request('/api/app-auth/me');
}
export function appAuthSetup(username, password) {
return request('/api/app-auth/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
}
// --- Admin User Management ---
export function getAppUsers() {
return request('/api/admin/users');
}
export function createAppUser(data) {
return request('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
export function updateAppUser(id, data) {
return request(`/api/admin/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
export function deleteAppUser(id) {
return request(`/api/admin/users/${id}`, { method: 'DELETE' });
}
export function getAvailableFolders() {
return request('/api/admin/available-folders');
}