236f36aae6
- 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>
466 lines
12 KiB
JavaScript
466 lines
12 KiB
JavaScript
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) {
|
|
let errMsg = data.error || data.message || `Request failed with status ${response.status}`;
|
|
if (typeof errMsg === 'object') errMsg = errMsg.message || errMsg.error || JSON.stringify(errMsg);
|
|
return { error: String(errMsg) };
|
|
}
|
|
|
|
// OF API sometimes returns 200 with error body like {code, message} instead of proper HTTP error
|
|
if (data && typeof data.code !== 'undefined' && !data.id) {
|
|
return { error: data.message || 'Request failed' };
|
|
}
|
|
|
|
return data;
|
|
} catch (err) {
|
|
return { error: err.message || 'Network error' };
|
|
}
|
|
}
|
|
|
|
function buildQuery(params) {
|
|
const query = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
query.set(key, value);
|
|
}
|
|
}
|
|
const str = query.toString();
|
|
return str ? `?${str}` : '';
|
|
}
|
|
|
|
export function getMe() {
|
|
return request('/api/me');
|
|
}
|
|
|
|
export function getFeed(beforePublishTime) {
|
|
const query = buildQuery({ beforePublishTime });
|
|
return request(`/api/feed${query}`);
|
|
}
|
|
|
|
export function getSubscriptions(offset) {
|
|
const query = buildQuery({ offset });
|
|
return request(`/api/subscriptions${query}`);
|
|
}
|
|
|
|
export function getUserPosts(userId, beforePublishTime) {
|
|
const query = buildQuery({ beforePublishTime });
|
|
return request(`/api/users/${userId}/posts${query}`);
|
|
}
|
|
|
|
export function getUser(username) {
|
|
return request(`/api/users/${username}`);
|
|
}
|
|
|
|
export function saveAuth(config) {
|
|
return request('/api/auth', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
export function getAuth() {
|
|
return request('/api/auth');
|
|
}
|
|
|
|
export function startDownload(userId, limit, resume, username) {
|
|
const body = {};
|
|
if (limit) body.limit = limit;
|
|
if (resume) body.resume = true;
|
|
if (username) body.username = username;
|
|
return request(`/api/download/${userId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
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`);
|
|
}
|
|
|
|
export function getActiveDownloads() {
|
|
return request('/api/download/active');
|
|
}
|
|
|
|
export function getDownloadCursor(userId) {
|
|
return request(`/api/download/${userId}/cursor`);
|
|
}
|
|
|
|
export function getDownloadHistory() {
|
|
return request('/api/download/history');
|
|
}
|
|
|
|
export function getGalleryFolders() {
|
|
return request('/api/gallery/folders');
|
|
}
|
|
|
|
export function getSettings() {
|
|
return request('/api/settings');
|
|
}
|
|
|
|
export function updateSettings(settings) {
|
|
return request('/api/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
|
|
export function rescanMedia() {
|
|
return request('/api/gallery/rescan', { method: 'POST' });
|
|
}
|
|
|
|
export function getRescanStatus() {
|
|
return request('/api/gallery/rescan/status');
|
|
}
|
|
|
|
export function generateThumbs() {
|
|
return request('/api/gallery/generate-thumbs', { method: 'POST' });
|
|
}
|
|
|
|
export function getThumbsStatus() {
|
|
return request('/api/gallery/generate-thumbs/status');
|
|
}
|
|
|
|
export function scanDuplicates(mode = 'everywhere') {
|
|
return request(`/api/gallery/scan-duplicates?mode=${mode}`, { method: 'POST' });
|
|
}
|
|
|
|
export function getDuplicateScanStatus() {
|
|
return request('/api/gallery/scan-duplicates/status');
|
|
}
|
|
|
|
export function getDuplicateGroups(offset = 0, limit = 20) {
|
|
const query = buildQuery({ offset, limit });
|
|
return request(`/api/gallery/duplicates${query}`);
|
|
}
|
|
|
|
export function cleanDuplicates() {
|
|
return request('/api/gallery/duplicates/clean', { method: 'POST' });
|
|
}
|
|
|
|
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',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
export function startCoomerScrape(config) {
|
|
return request('/api/scrape/coomer', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
}
|
|
|
|
export function startMediaLinkScrape(config) {
|
|
return request('/api/scrape/medialink', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(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');
|
|
}
|
|
|
|
export function getScrapeJob(jobId) {
|
|
return request(`/api/scrape/jobs/${jobId}`);
|
|
}
|
|
|
|
export function cancelScrapeJob(jobId) {
|
|
return request(`/api/scrape/jobs/${jobId}/cancel`, { method: 'POST' });
|
|
}
|
|
|
|
export function detectForumPages(url, cookies) {
|
|
return request('/api/scrape/forum/detect-pages', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
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');
|
|
}
|