Files
OFApp/client/src/api.js
T
Trey T 236f36aae6 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>
2026-04-16 07:48:10 -05:00

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