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'); }