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:
+274
-6
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user