import {emptyToNull} from "./components/Common"; import {toast} from "react-semantic-toasts-2"; import _ from "lodash"; import { API_URI, ARCHIVES_API, COLLECTIONS_API, DEFAULT_LIMIT, Downloaders, OTP_API, VIDEOS_API, ZIM_API } from "./components/Vars"; function timeoutPromise(ms, promise) { // Create a timeout wrapper around a promise. If the timeout is reached, throw an error. Otherwise, return // the results of the promise. return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("promise timeout")) }, ms); promise.then((res) => { clearTimeout(timeoutId); resolve(res); }, (err) => { clearTimeout(timeoutId); reject(err); }); }) } export class ApiDownError extends Error { } const apiDownError = new ApiDownError('API is down'); async function apiCall(url, method, body, ms = 60_000) { let init = {method}; if (body !== undefined) { init.body = JSON.stringify(body); } let promise; try { // Create a fetch promise, this will be awaited by the timeout. promise = fetch(url, init); // await the response or error. const response = await timeoutPromise(ms, promise); if (200 <= response.status && response.status < 300) { // Request was successful. return response; } if (response.status === 502) { // API is down. throw apiDownError; } // Request encountered an error. let copy = response.clone(); const content = await copy.json(); console.error('API error response json', content); const code = content['code']; if (response.status === 403 && code === 'WROL_MODE_ENABLED') { toast({ type: 'error', title: 'WROL Mode is enabled!', description: 'This functionality is disabled while WROL Mode is enabled!', time: 5000, }); } return response; } catch (e) { if (e instanceof ApiDownError) { // Throw ApiDownError immediately without logging. throw e; } // Ignore SyntaxError because they happen when the API is down. if (!(e instanceof SyntaxError)) { console.error(e); } throw e; } } // Convenience functions for API calls. let apiGet = async (url) => { return await apiCall(url, 'GET') }; let apiDelete = async (url) => { return await apiCall(url, 'DELETE') }; let apiPost = async (url, body) => { return await apiCall(url, 'POST', body) }; let apiPut = async (url, body) => { return await apiCall(url, 'PUT', body) }; let apiPatch = async (url, body) => { return await apiCall(url, 'PATCH', body) }; async function getErrorMessage(response, fallbackMessage) { try { const json = await response.clone().json(); return json.error || json.message || fallbackMessage; } catch (error) { // Server did not response with JSON. Return default message. return fallbackMessage; } } export async function updateChannel(id, channel) { channel = emptyToNull(channel); return await apiPut(`${VIDEOS_API}/channels/${id}`, channel); } export async function createChannel(channel) { channel = emptyToNull(channel); return await apiPost(`${VIDEOS_API}/channels`, channel); } export async function deleteChannel(channelId) { const response = await apiDelete(`${VIDEOS_API}/channels/${channelId}`); if (response.status !== 204) { const message = await getErrorMessage(response, 'Failed to delete channel.'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response; } export async function getChannels() { const response = await apiGet(`${VIDEOS_API}/channels`); if (response.status === 200) { return (await response.json())['channels']; } else { const message = await getErrorMessage(response, 'Failed to get channels.'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } } export async function getChannel(id) { const response = await apiGet(`${VIDEOS_API}/channels/${id}`); return (await response.json())['channel']; } export async function tagChannel(channelId, tagName, directory) { const body = { tag_name: tagName, } if (directory) { body['directory'] = directory; } const response = await apiPost(`${VIDEOS_API}/channels/${channelId}/tag`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to tag channel. See server logs.'); toast({ type: 'error', title: 'Tagging Channel Failed', description: message, time: 5000, }); } } export async function tagChannelInfo(channelId, tagName) { channelId = channelId ? parseInt(channelId) : null; const body = {channel_id: channelId, tag_name: tagName}; const response = await apiPost(`${VIDEOS_API}/tag_info`, body); if (response.ok) { return (await response.json()).videos_destination; } const message = await getErrorMessage(response, 'Failed to get channel tag info.'); toast({ type: 'error', title: 'Getting Channel Tag Info Failed', description: message, time: 5000, }); } export async function searchVideos(offset, limit, channelId, searchStr, order_by, tagNames, headline) { // Build a search query to retrieve a list of videos from the API offset = parseInt(offset || 0); limit = parseInt(limit || DEFAULT_LIMIT); let body = {offset, limit, order_by: order_by || 'rank'}; if (searchStr) { body['search_str'] = searchStr; } if (channelId) { body['channel_id'] = parseInt(channelId); } if (tagNames) { body['tag_names'] = tagNames; } if (headline) { body['headline'] = true; } console.debug('searching videos', body); const response = await apiPost(`${VIDEOS_API}/search`, body); if (response.status === 200) { let data = await response.json(); return [data['file_groups'], data['totals']['file_groups']]; } else { const message = await getErrorMessage(response, 'Failed to search videos.'); toast({ type: 'error', title: 'Searching Videos failed', description: message, time: 5000, }); return [[], 0]; } } export async function getVideo(fileGroupId) { const response = await apiGet(`${VIDEOS_API}/${fileGroupId}`); let data = await response.json(); return [data['file_group'], data['prev'], data['next']]; } export async function getVideoComments(fileGroupId) { const response = await apiGet(`${VIDEOS_API}/${fileGroupId}/comments`); let data = await response.json(); return { comments: data.comments, } } export async function getVideoCaptions(fileGroupId) { const response = await apiGet(`${VIDEOS_API}/${fileGroupId}/captions`); let data = await response.json(); return { captions: data.captions, } } export async function deleteFileGroups(fileGroupIds, force = false) { if (!fileGroupIds || fileGroupIds.length === 0) { toast({ type: 'error', title: 'Empty request', description: 'Unable to delete FileGroups because no IDs were passed.', time: 5000, }); return; } console.info(`Deleting FileGroups: ${fileGroupIds}`); const body = {file_group_ids: fileGroupIds, force}; const response = await apiPost(`${API_URI}/files/delete_groups`, body); if (response.ok) { return response; } try { const json = await response.clone().json(); if (json.code === 'FILE_GROUP_IS_TAGGED' && json.file_groups) { return {tagged: true, file_groups: json.file_groups}; } } catch (e) { // Not JSON, fall through to generic error. } const message = await getErrorMessage(response, 'Failed to delete files.'); toast({ type: 'error', title: 'Delete failed', description: message, time: 5000, }); throw Error(message); } export async function downloadVideoMetadata(videoUrl) { const downloadData = { urls: [videoUrl], downloader: Downloaders.Video, settings: {download_metadata_only: true}, }; return await postDownload(downloadData); } export async function getStatus() { const response = await apiGet(`${API_URI}/status`); if (response.status === 200) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not get server status.'); toast({ type: 'error', title: 'Fetching Status Failed', description: message, time: 5000, }); } } export async function getSettings() { const response = await apiGet(`${API_URI}/settings`); if (response.ok) { const content = await response.json(); return { archive_destination: content.archive_destination, download_manager_disabled: content.download_manager_disabled, download_manager_stopped: content.download_manager_stopped, download_on_startup: content.download_on_startup, download_timeout: content.download_timeout, download_wait: content.download_wait, download_window_start: content.download_window_start, download_window_end: content.download_window_end, hotspot_device: content.hotspot_device, hotspot_on_startup: content.hotspot_on_startup, hotspot_password: content.hotspot_password, hotspot_ssid: content.hotspot_ssid, hotspot_status: content.hotspot_status, check_for_upgrades: content.check_for_upgrades, ignore_outdated_zims: content.ignore_outdated_zims, ignored_directories: content.ignored_directories, log_level: content.log_level, map_default_location: content.map_default_location, map_destination: content.map_destination, nav_color: content.nav_color, media_directory: content.media_directory, require_cookies_unlocked: content.require_cookies_unlocked, require_media_mounted: content.require_media_mounted, save_ffprobe_json: content.save_ffprobe_json, tags_directory: content.tags_directory, throttle_on_startup: content.throttle_on_startup, throttle_status: content.throttle_status, timezone: content.timezone, videos_destination: content.videos_destination, wrol_mode: content.wrol_mode, zims_destination: content.zims_destination, } } else { const message = await getErrorMessage(response, 'Could not get settings.'); toast({ type: 'error', title: 'Fetching Settings Failed', description: message, time: 5000, }); } } export async function saveSettings(settings) { const response = await apiPatch(`${API_URI}/settings`, settings); if (!response.ok) { const message = await getErrorMessage(response, 'Could not save settings.'); toast({ type: 'error', title: 'Saving Settings Failed', description: message, time: 5000, }); } return response } export async function getDownloads() { const response = await apiGet(`${API_URI}/download`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not get downloads.'); toast({ type: 'error', title: 'Getting Downloads Failed', description: message, time: 5000, }); } return response } export async function getConfigs() { const response = await apiGet(`${API_URI}/config`); if (response.ok) { const data = await response.json(); return { configs: data.configs, } } else { const message = await getErrorMessage(response, 'Could not get configs.'); toast({ type: 'error', title: 'Getting Configs Failed', description: message, time: 5000, }); } return response } export async function postImportConfig(fileName) { const body = {file_name: fileName}; const response = await apiPost(`${API_URI}/config/import`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Could not import config.'); toast({ type: 'error', title: 'Importing config Failed', description: message, time: 5000, }); } return response } export async function postDumpConfig(fileName) { const body = {file_name: fileName, overwrite: true}; const response = await apiPost(`${API_URI}/config/dump`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Could not dump config.'); toast({ type: 'error', title: 'Dumping config failed', description: message, time: 5000, }); } return response } export async function getConfig(fileName) { const response = await apiGet(`${API_URI}/config?file_name=${fileName}`); return (await response.json())['config']; } export async function getConfigText(fileName) { const response = await apiGet(`${API_URI}/config?file_name=${fileName}`); return (await response.json())['config_text']; } export async function getConfigBackups(fileName) { const response = await apiGet(`${API_URI}/config/backups?file_name=${fileName}`); if (response.ok) { return await response.json(); } return {dates: []}; } export async function postConfigBackupPreview(fileName, backupDate, mode) { const body = {file_name: fileName, backup_date: backupDate, mode}; const response = await apiPost(`${API_URI}/config/backup/preview`, body); if (response.ok) { return await response.json(); } const message = await getErrorMessage(response, 'Could not preview backup.'); toast({type: 'error', title: 'Preview failed', description: message, time: 5000}); return null; } export async function postConfigBackupImport(fileName, backupDate, mode) { const body = {file_name: fileName, backup_date: backupDate, mode}; const response = await apiPost(`${API_URI}/config/backup/import`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Could not import backup.'); toast({type: 'error', title: 'Backup import failed', description: message, time: 5000}); } return response; } export async function fetchVideoDownloaderConfig() { return await getConfig('videos_downloader.yaml'); } export async function fetchVideoDownloadDefaults() { const response = await apiGet(`${VIDEOS_API}/download-defaults`); return await response.json(); } export async function updateConfig(fileName, config) { const body = {config}; const response = await apiPost(`${API_URI}/config?file_name=${fileName}`, body); if (!response.ok) { throw Error('Failed to save config'); } } export async function updateVideoDownloaderConfig(config) { return await updateConfig('videos_downloader.yaml', config); } export async function fetchArchiveDownloaderConfig() { return await getConfig('archives_downloader.yaml'); } export async function updateArchiveDownloaderConfig(config) { return await updateConfig('archives_downloader.yaml', config); } export async function postArchiveFileFormat(archive_file_format) { const body = {archive_file_format}; return await apiPost(`${ARCHIVES_API}/file_format`, body); } export async function fetchArchiveBrowsers() { const response = await apiGet(`${ARCHIVES_API}/browsers`); if (response.ok) { return await response.json(); } return {browsers: [], available: false}; } export async function getArchiveStatistics() { const response = await apiGet(`${ARCHIVES_API}/statistics`); if (response.status === 200) { return (await response.json())['statistics']; } else { const message = await getErrorMessage(response, 'Could not get archive statistics.'); toast({ type: 'error', title: 'Getting Archive Statistics Failed', description: message, time: 5000, }); } return response } export async function updateVideoDownloaderSettings(settings) { const response = await apiPut(`${API_URI}/videos/settings`, settings); if (response.status === 200) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to save settings.'); toast({ type: 'error', title: 'Saving Settings Failed', description: message, time: 5000, }); } } export async function validateRegex(regex) { const response = await apiPost(`${API_URI}/valid_regex`, {regex}); try { return (await response.json())['valid']; } catch (e) { return false; } } export async function getVideosStatistics() { const response = await apiGet(`${VIDEOS_API}/statistics`); if (response.status === 200) { return (await response.json())['statistics']; } else { const message = await getErrorMessage(response, 'Could not get statistics.'); toast({ type: 'error', title: 'Getting Statistics Failed', description: message, time: 5000, }); } return response } export async function refreshChannel(channelId) { let url = `${VIDEOS_API}/channels/refresh/${channelId}`; const response = await apiPost(url); if (!response.ok) { const message = await getErrorMessage(response, "Failed to refresh this channel's directory"); toast({ type: 'error', title: 'Failed to refresh', description: message, time: 5000, }) } return response; } export async function encryptOTP(otp, plaintext) { let body = {otp, plaintext}; const response = await apiPost(`${OTP_API}/encrypt_otp`, body); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to encrypt OTP. See server logs.'); toast({ type: 'error', title: 'Failed to encrypt OTP', description: message, time: 5000, }) } } export async function decryptOTP(otp, ciphertext) { let body = {otp, ciphertext}; const response = await apiPost(`${OTP_API}/decrypt_otp`, body); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to decrypt OTP. See server logs.'); toast({ type: 'error', title: 'Failed to decrypt OTP', description: message, time: 5000, }) } } export async function getCategories() { const response = await apiGet(`${API_URI}/inventory/categories`); if (response.ok) { return (await response.json())['categories']; } else { const message = await getErrorMessage(response, 'Could not get categories'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } } export async function getBrands() { const response = await apiGet(`${API_URI}/inventory/brands`); if (response.ok) { return (await response.json())['brands']; } else { const message = await getErrorMessage(response, 'Could not get brands'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } } export async function getInventories() { const response = await apiGet(`${API_URI}/inventory`); if (response.ok) { return (await response.json())['inventories']; } else { const message = await getErrorMessage(response, 'Could not get inventories'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } } export async function getInventory(inventoryId) { const response = await apiGet(`${API_URI}/inventory/${inventoryId}`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not get inventory'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } } export async function saveInventory(inventory) { const response = await apiPost(`${API_URI}/inventory`, inventory); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to save inventory'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function updateInventory(inventoryId, inventory) { delete inventory['id']; const response = await apiPut(`${API_URI}/inventory/${inventoryId}`, inventory); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to update inventory'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function deleteInventory(inventoryId) { const response = await apiDelete(`${API_URI}/inventory/${inventoryId}`); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to delete inventory'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function getItems(inventoryId) { const response = await apiGet(`${API_URI}/inventory/${inventoryId}/item`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to get items'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function saveItem(inventoryId, item) { const response = await apiPost(`${API_URI}/inventory/${inventoryId}/item`, item); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to save item'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function updateItem(itemId, item) { item = emptyToNull(item); const response = await apiPut(`${API_URI}/inventory/item/${itemId}`, item); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to update item'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function deleteItems(itemIds) { let i = itemIds.join(','); const response = await apiDelete(`${API_URI}/inventory/item/${i}`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to delete items'); toast({ type: 'error', title: 'Unexpected server response', description: message, time: 5000, }); } return response } export async function searchArchives(offset, limit, domain, searchStr, order, tagNames, headline) { // Build a search query to retrieve a list of videos from the API offset = parseInt(offset || 0); limit = parseInt(limit || DEFAULT_LIMIT); let body = {offset, limit}; if (domain) { body['domain'] = domain; } if (searchStr) { body['search_str'] = searchStr; } if (order) { body['order_by'] = order; } if (tagNames) { body['tag_names'] = tagNames; } if (headline) { body['headline'] = true; } console.debug('searching archives', body); const response = await apiPost(`${ARCHIVES_API}/search`, body); if (response.ok) { let data = await response.json(); return [data['file_groups'], data['totals']['file_groups']]; } else { const message = await getErrorMessage(response, 'Cannot search archives. See server logs.'); toast({ type: 'error', title: 'Unable to search archives', description: message, time: 5000, }); return [[], 0]; } } export async function fetchDomains() { const response = await apiGet(`${COLLECTIONS_API}?kind=domain`); if (response.ok) { let data = await response.json(); return [data['collections'], data['totals']['collections']]; } else { const message = await getErrorMessage(response, 'Unable to fetch Domains. See server logs.'); toast({ type: 'error', title: 'Domains Error', description: message, time: 5000, }); } } export async function fetchAuthors() { const response = await apiGet(`${COLLECTIONS_API}?kind=author`); if (response.ok) { let data = await response.json(); return [data['collections'], data['totals']['collections']]; } else { const message = await getErrorMessage(response, 'Unable to fetch Authors. See server logs.'); toast({ type: 'error', title: 'Authors Error', description: message, time: 5000, }); } } export async function fetchSubjects() { const response = await apiGet(`${COLLECTIONS_API}?kind=subject`); if (response.ok) { let data = await response.json(); return [data['collections'], data['totals']['collections']]; } else { const message = await getErrorMessage(response, 'Unable to fetch Subjects. See server logs.'); toast({ type: 'error', title: 'Subjects Error', description: message, time: 5000, }); } } export async function fetchChannels() { const response = await apiGet(`${COLLECTIONS_API}?kind=channel`); if (response.ok) { let data = await response.json(); return [data['collections'], data['totals']['collections']]; } else { const message = await getErrorMessage(response, 'Unable to fetch Channels. See server logs.'); toast({ type: 'error', title: 'Channels Error', description: message, time: 5000, }); } } export async function getDomain(domainId) { const response = await apiGet(`${COLLECTIONS_API}/${domainId}`); if (response.ok) { const data = await response.json(); return data['collection']; } else { const message = await getErrorMessage(response, 'Unable to fetch Domain. See server logs.'); toast({ type: 'error', title: 'Domain Error', description: message, time: 5000, }); throw new Error(message); } } export async function updateDomain(domainId, updates) { updates = emptyToNull(updates); const response = await apiPut(`${COLLECTIONS_API}/${domainId}`, updates); if (response.ok) { const data = await response.json(); return data['collection']; } else { const message = await getErrorMessage(response, 'Unable to update Domain. See server logs.'); toast({ type: 'error', title: 'Domain Update Error', description: message, time: 5000, }); throw new Error(message); } } export async function getCollectionTagInfo(collectionId, tagName) { const body = { tag_name: tagName, } const response = await apiPost(`${COLLECTIONS_API}/${collectionId}/tag_info`, body); if (response.ok) { const data = await response.json(); return data; } else { const message = await getErrorMessage(response, 'Unable to get tag info. See server logs.'); toast({ type: 'error', title: 'Tag Info Error', description: message, time: 5000, }); throw new Error(message); } } export async function tagDomain(domainId, tagName, directory) { const body = { tag_name: tagName, } if (directory) { body['directory'] = directory; } const response = await apiPost(`${COLLECTIONS_API}/${domainId}/tag`, body); if (response.ok) { const data = await response.json(); return data; } else { const message = await getErrorMessage(response, 'Unable to tag Domain. See server logs.'); toast({ type: 'error', title: 'Domain Tag Error', description: message, time: 5000, }); throw new Error(message); } } export async function deleteDomain(domainId) { const response = await apiDelete(`${COLLECTIONS_API}/${domainId}`); if (response.status !== 204) { const message = await getErrorMessage(response, 'Failed to delete domain.'); toast({ type: 'error', title: 'Delete Failed', description: message, time: 5000, }); throw new Error(message); } return response; } export async function refreshDomain(domainId) { let url = `${COLLECTIONS_API}/${domainId}/refresh`; const response = await apiPost(url); if (!response.ok) { const message = await getErrorMessage(response, "Failed to refresh this domain's directory"); toast({ type: 'error', title: 'Failed to refresh', description: message, time: 5000, }); } else { toast({ type: 'success', title: 'Domain refresh started', description: 'The domain directory is being refreshed', time: 3000, }); } return response; } export async function previewCollectionReorganization(collectionId) { const response = await apiGet(`${COLLECTIONS_API}/${collectionId}/reorganize/preview`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Unable to preview reorganization. See server logs.'); toast({ type: 'error', title: 'Reorganization Preview Error', description: message, time: 5000, }); throw new Error(message); } } export async function executeCollectionReorganization(collectionId) { const response = await apiPost(`${COLLECTIONS_API}/${collectionId}/reorganize`); if (response.ok) { const data = await response.json(); if (data.job_id) { toast({ type: 'success', title: 'Reorganization Started', description: 'Files are being reorganized in the background', time: 3000, }); } else { toast({ type: 'info', title: 'No Changes Needed', description: data.message || 'No files need reorganization', time: 3000, }); } return data; } else { const message = await getErrorMessage(response, 'Unable to start reorganization. See server logs.'); toast({ type: 'error', title: 'Reorganization Error', description: message, time: 5000, }); throw new Error(message); } } export async function getReorganizationStatus(collectionId, jobId) { const response = await apiGet(`${COLLECTIONS_API}/${collectionId}/reorganize/status/${jobId}`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Unable to get reorganization status.'); throw new Error(message); } } // Batch Reorganization API Functions export async function previewBatchReorganization(kind) { const endpoint = kind === 'channel' ? 'channels' : 'domains'; const response = await apiGet(`${COLLECTIONS_API}/reorganize/${endpoint}`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, `Unable to get ${kind}s needing reorganization.`); toast({ type: 'error', title: 'Batch Reorganization Preview Error', description: message, time: 5000, }); throw new Error(message); } } export async function executeBatchReorganization(kind) { const endpoint = kind === 'channel' ? 'channels' : 'domains'; const response = await apiPost(`${COLLECTIONS_API}/reorganize/${endpoint}`); if (response.ok) { const data = await response.json(); if (data.batch_job_id) { toast({ type: 'success', title: 'Batch Reorganization Started', description: `${data.collection_count} ${kind}s are being reorganized`, time: 3000, }); } else { toast({ type: 'info', title: 'No Changes Needed', description: data.message || `No ${kind}s need reorganization`, time: 3000, }); } return data; } else { const message = await getErrorMessage(response, `Unable to start batch reorganization. See server logs.`); toast({ type: 'error', title: 'Batch Reorganization Error', description: message, time: 5000, }); throw new Error(message); } } export async function getBatchReorganizationStatus(batchJobId) { const response = await apiGet(`${COLLECTIONS_API}/reorganize/batch/status/${batchJobId}`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Unable to get batch reorganization status.'); throw new Error(message); } } export async function fetchCollectionConflicts(collectionId) { const response = await apiGet(`${COLLECTIONS_API}/${collectionId}/reorganize/conflicts`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Unable to fetch conflicts. See server logs.'); toast({ type: 'error', title: 'Conflict Fetch Error', description: message, time: 5000, }); throw new Error(message); } } export async function getArchive(fileGroupId) { const response = await apiGet(`${ARCHIVES_API}/${fileGroupId}`); if (response.ok) { const data = await response.json(); return { file_group: data['file_group'], history: data['history'], } } else { const message = await getErrorMessage(response, 'Unable to get Archive. See server logs.'); toast({ type: 'error', title: 'Archive Error', description: message, time: 5000, }); } } export async function generateArchiveScreenshot(fileGroupId) { const response = await apiPost(`${ARCHIVES_API}/${fileGroupId}/generate_screenshot`); if (response.ok) { toast({ type: 'success', title: 'Screenshot Generation Queued', description: 'Screenshot generation has been queued. This may take a moment.', time: 3000, }); return true; } else { const message = await getErrorMessage(response, 'Failed to queue screenshot generation.'); toast({ type: 'error', title: 'Screenshot Generation Error', description: message, time: 5000, }); return false; } } export async function postDownload(downloadData) { if (!downloadData.downloader) { toast({ type: 'error', title: 'Failed to submit download', description: 'downloader is required, but was not provided', time: 5000, }) throw new Error('downloader is required, but was not provided'); } const body = { urls: downloadData.urls, destination: downloadData.destination, downloader: downloadData.downloader, sub_downloader: downloadData.sub_downloader || null, frequency: downloadData.frequency || null, tag_names: downloadData.tag_names || null, settings: downloadData.settings, }; const response = await apiPost(`${API_URI}/download`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to create Download. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); throw new Error('Failed to create download'); } return response; } export async function putDownload(download_id, downloadData) { if (!downloadData.downloader) { toast({ type: 'error', title: 'Failed to submit download', description: 'downloader is required, but was not provided', time: 5000, }) throw new Error('downloader is required, but was not provided'); } const response = await apiPut(`${API_URI}/download/${download_id}`, downloadData); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to update Download. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); throw new Error('Failed to update download'); } return response; } export async function killDownload(download_id) { const response = await apiPost(`${API_URI}/download/${download_id}/kill`); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to stop Download. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); } return response; } export async function restartDownload(download_id) { const response = await apiPost(`${API_URI}/download/${download_id}/restart`); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to restart Download. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); } return response } export async function killDownloads() { const response = await apiPost(`${API_URI}/download/kill`); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to stop downloading. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); } return response; } export async function startDownloads() { const response = await apiPost(`${API_URI}/download/enable`); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to start downloading. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); } return response; } export async function getDownloaders() { try { const response = await apiGet(`${API_URI}/downloaders`); // Not toasting because this will happen often. return await response.json(); } catch (e) { return {downloaders: []}; } } export async function deleteDownload(downloadId) { try { const response = await apiDelete(`${API_URI}/download/${downloadId}`); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to delete the download. See server logs.'); toast({ type: 'error', title: 'Download Error', description: message, time: 5000, }); } } catch (e) { return null; } } export async function filesSearch(offset, limit, searchStr, mimetypes, model, tagNames, headline, months, fromYear, toYear, anyTag, order) { const body = {search_str: searchStr, offset: parseInt(offset), limit: parseInt(limit), any_tag: anyTag}; if (mimetypes) { body['mimetypes'] = mimetypes; } if (model) { body['model'] = model; } if (tagNames) { body['tag_names'] = tagNames; } if (headline) { body['headline'] = true; } if (!_.isEmpty(months)) { body['months'] = months.map(i => parseInt(i)); } if (fromYear) { body['from_year'] = fromYear; } if (toYear) { body['to_year'] = toYear; } if (order) { body['order'] = order; } console.info('searching files', body); const response = await apiPost(`${API_URI}/files/search`, body); if (response.ok) { let data = await response.json(); let [file_groups, total] = [data['file_groups'], data['totals']['file_groups']]; return [file_groups, total]; } else { const message = await getErrorMessage(response, 'Cannot search files. See server logs.'); toast({ type: 'error', title: 'Unable to search files', description: message, time: 5000, }); return [null, null]; } } export async function refreshFiles(paths) { let response; if (Array.isArray(paths) && paths.length > 0) { // Refresh user-selected files/directories only. const body = {paths}; console.info(`Refreshing: ${paths}`); response = await apiPost(`${API_URI}/files/refresh`, body); } else { console.info(`Refreshing all files`); response = await apiPost(`${API_URI}/files/refresh`); } if (!response.ok) { const message = await getErrorMessage(response, 'Cannot refresh files. See server logs.'); toast({ type: 'error', title: 'Files Error', description: message, time: 5000, }); } return response; } export async function makeDirectory(path) { const body = {path: path}; const response = await apiPost(`${API_URI}/files/directory`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to create directory. See server logs.'); toast({ type: 'error', title: 'Unable to create directory', description: message, time: 5000, }); } } export async function getFiles(directories) { console.debug(`getFiles ${directories}`); let body = {directories: directories || []}; const response = await apiPost(`${API_URI}/files`, body); // Not toasting because this will happen often. let {files} = await response.json(); return files; } export async function getFile(path, skipTracking = false) { let body = {file: path, skip_tracking: skipTracking}; const response = await apiPost(`${API_URI}/files/file`, body); if (response.ok) { const {file} = await response.json(); return file; } else { const message = await getErrorMessage(response, 'Failed to get file data. See server logs.'); toast({ type: 'error', title: 'Files Error', description: message, time: 5000, }); } } export async function deleteFile(paths, force = false) { let body = {paths: paths, force: force}; const response = await apiPost(`${API_URI}/files/delete`, body); if (!response.ok) { try { const json = await response.clone().json(); if (json.code === 'FILE_GROUP_IS_TAGGED' && json.file_groups) { return {tagged: true, file_groups: json.file_groups}; } } catch (e) { // Not JSON, fall through to generic error. } const message = await getErrorMessage(response, 'Failed to delete file. See server logs.'); toast({ type: 'error', title: 'Files Error', description: message, time: 5000, }); } return response } export async function getArchiveContents(path) { const response = await apiPost(`${API_URI}/files/zip/contents`, {path}); if (response.ok) { const {contents} = await response.json(); return contents; } const message = await getErrorMessage(response, 'Could not read archive contents.'); toast({type: 'error', title: 'Archive Error', description: message, time: 5000}); } export function downloadArchiveMember(path, member) { const params = new URLSearchParams({path, member}); window.open(`${API_URI}/files/zip/download?${params}`, '_blank'); } export function getArchiveMemberUrl(path, member) { const params = new URLSearchParams({path, member}); return `${API_URI}/files/zip/download?${params}`; } export async function fetchFilesProgress() { const response = await apiGet(`${API_URI}/files/worker_status`); if (response.ok) { const json = await response.json(); return json['status']; } } export async function setHotspot(on) { let response; if (on) { response = await apiPost(`${API_URI}/hotspot/on`); } else { response = await apiPost(`${API_URI}/hotspot/off`); } if (response.ok) { return null; } else { const content = await response.json(); const code = content['code']; if (code === 'NATIVE_ONLY') { toast({ type: 'error', title: 'Unsupported!', description: 'Hotspot is only supported outside Docker!', time: 5000, }); } else { const message = await getErrorMessage(response, 'Could not modify hotspot. See server logs.'); toast({ type: 'error', title: 'Hotspot Error', description: message, time: 5000, }); } } } export async function setThrottle(on) { let response; if (on) { response = await apiPost(`${API_URI}/throttle/on`); } else { response = await apiPost(`${API_URI}/throttle/off`); } if (response.ok) { return null; } else { const content = await response.json(); const code = content['code']; if (code === 'NATIVE_ONLY') { toast({ type: 'error', title: 'Unsupported!', description: 'Throttle is only supported on a Raspberry Pi!', time: 5000, }); } else { const message = await getErrorMessage(response, 'Could not modify throttle. See server logs.'); toast({type: 'error', title: 'Throttle Error', description: message, time: 5000}); } } } export async function getMapFiles() { const response = await apiGet(`${API_URI}/map/files`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not get map files'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function deleteMapFile(filename) { const response = await apiDelete(`${API_URI}/map/files/${encodeURIComponent(filename)}`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not delete map file'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function fetchMapSubscriptions() { const response = await apiGet(`${API_URI}/map/subscribe`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not get map subscriptions'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function mapSubscribe(name, region) { const body = {name, region}; const response = await apiPost(`${API_URI}/map/subscribe`, body); if (response.ok) { return true; } else { const message = await getErrorMessage(response, 'Could not subscribe to map region'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); return false; } } export async function mapUnsubscribe(region) { const response = await apiDelete(`${API_URI}/map/subscribe/${encodeURIComponent(region)}`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not unsubscribe from map region'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function getMapPins() { const response = await apiGet(`${API_URI}/map/pins`); if (response.ok) { return await response.json(); } } export async function addMapPin(lat, lon, label, color) { const body = {lat, lon, label, color}; const response = await apiPost(`${API_URI}/map/pins`, body); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Could not add map pin'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function deleteMapPin(pinId) { const response = await apiDelete(`${API_URI}/map/pins/${pinId}`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not delete map pin'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function updateMapPin(pinId, label, color) { const body = {label, color}; const response = await apiPut(`${API_URI}/map/pins/${pinId}`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Could not update map pin'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } } export async function setMapDefaultLocation(lat, lon, zoom) { try { await updateConfig('wrolpi.yaml', {map_default_location: {lat, lon, zoom}}); toast({type: 'success', title: 'Default Location', description: 'Default map location saved', time: 3000}); return true; } catch (e) { toast({type: 'error', title: 'Map Error', description: 'Could not set default location', time: 5000}); return false; } } export async function searchMap(query, limit = 12, offset = 0, lat = null, lon = null) { let url = `${API_URI}/map/search?q=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}`; if (lat !== null && lon !== null) { url += `&lat=${lat}&lon=${lon}`; } const response = await apiGet(url); if (!response.ok) { return {results: [], total: 0}; } return await response.json(); } export async function getMapSearchStatus() { const response = await apiGet(`${API_URI}/map/search/status`); if (!response.ok) { return {indexed: [], missing: []}; } return await response.json(); } export async function searchEstimateMap(search_str) { if (!search_str) return {mapPlaces: 0}; const response = await apiGet(`${API_URI}/map/search/estimate?q=${encodeURIComponent(search_str)}`); if (!response.ok) return {mapPlaces: 0}; const data = await response.json(); return {mapPlaces: data.map_places || 0}; } export async function rebuildMapSearchIndex(filename) { const response = await apiPost(`${API_URI}/map/search/rebuild/${encodeURIComponent(filename)}`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not rebuild search index'); toast({type: 'error', title: 'Map Error', description: message, time: 5000}); } else { toast({type: 'success', title: 'Search Index', description: 'Search index build started', time: 3000}); } } export async function clearCompletedDownloads() { const response = await apiPost(`${API_URI}/download/clear_completed`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not clear completed downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function deleteOnceDownloads() { const response = await apiPost(`${API_URI}/download/delete_once`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not delete once downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function retryOnceDownloads() { const response = await apiPost(`${API_URI}/download/retry_once`); if (!response.ok) { const message = await getErrorMessage(response, 'Could not retry once downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function batchDeleteDownloads(downloadIds) { const response = await apiPost(`${API_URI}/download/batch/delete`, {download_ids: downloadIds}); if (!response.ok) { const message = await getErrorMessage(response, 'Could not delete downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function batchRetryDownloads(downloadIds) { const response = await apiPost(`${API_URI}/download/batch/retry`, {download_ids: downloadIds}); if (!response.ok) { const message = await getErrorMessage(response, 'Could not retry downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function batchClearDownloads(downloadIds) { const response = await apiPost(`${API_URI}/download/batch/clear`, {download_ids: downloadIds}); if (!response.ok) { const message = await getErrorMessage(response, 'Could not clear downloads! See server logs.'); toast({type: 'error', title: 'Downloads Error', description: message, time: 5000}); } return response } export async function getStatistics() { const response = await apiGet(`${API_URI}/statistics`); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Unable to get file statistics'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function getEvents(after) { let uri = `${API_URI}/events/feed`; if (after) { uri = `${uri}?after=${encodeURIComponent(after)}` } const response = await apiGet(uri); if (response.ok) { return await response.json(); } // Not toasting because this happens often. return response } export async function getTags(tagNames) { let uri = `${API_URI}/tag`; if (tagNames && tagNames.length > 0) { const params = new URLSearchParams(); (typeof tagNames === 'string' ? [tagNames] : tagNames).forEach(name => params.append('tag_names', name)); uri = `${uri}?${params.toString()}`; } const response = await apiGet(uri); if (response.ok) { const body = await response.json(); return body; } // Not toasting because this happens often. return response } export async function getRecentTags() { const uri = `${API_URI}/tag/recent`; const response = await apiGet(uri); if (response.ok) { const body = await response.json(); return body['tag_names'] || []; } return []; } export async function tagFileGroup(fileGroup, name) { const body = {tag_name: name}; const {id, primary_path, path} = fileGroup; if (id) { body['file_group_id'] = id; } else if (primary_path) { body['file_group_primary_path'] = primary_path; } else if (path) { body['file_group_primary_path'] = path; } const uri = `${API_URI}/files/tag`; const response = await apiPost(uri, body); if (!response.ok) { const message = await getErrorMessage(response, 'Unable to add tag'); toast({type: 'error', title: 'Tag Error', description: message, time: 5000}); } } export async function untagFileGroup(fileGroup, name) { const body = {tag_name: name}; const {id, primary_path, path} = fileGroup; if (id) { body['file_group_id'] = id; } else if (primary_path) { body['file_group_primary_path'] = primary_path; } else if (path) { body['file_group_primary_path'] = path; } const uri = `${API_URI}/files/untag`; const response = await apiPost(uri, body) if (!response.ok) { const message = await getErrorMessage(response, 'Unable to untag'); toast({type: 'error', title: 'Tag Error', description: message, time: 5000}); } } export async function saveTag(name, color, id) { const body = {name: name, color: color}; let uri = `${API_URI}/tag`; if (id) { uri = `${uri}/${id}`; } const response = await apiPost(uri, body); if (id && response.status === 200) { toast({ type: 'info', title: 'Saved tag', description: `Saved tag: ${name}`, time: 2000, }); } else if (response.status === 201) { toast({ type: 'info', title: 'Created new tag', description: `Created new tag: ${name}`, time: 2000, }); } else { const message = await getErrorMessage(response, 'Unable to save tag'); toast({type: 'error', title: 'Tag Error', description: message, time: 5000}); } } export async function deleteTag(id, name) { const uri = `${API_URI}/tag/${id}`; const response = await apiDelete(uri); if (response.status === 400) { const content = await response.json(); const message = await getErrorMessage(response, 'Cannot delete, Tag is used'); if (content['code'] === 'USED_TAG') { toast({ type: 'error', title: 'Error!', description: message, time: 5000, }) } } else if (!response.ok) { console.error('Failed to delete tag'); const message = await getErrorMessage(response, `Unable to delete tag: ${name}`); toast({type: 'error', title: 'Tag Error', description: message, time: 5000}); } } export async function sendNotification(message, url) { const body = {message, url}; const response = await apiPost(`${API_URI}/notify`, body); if (response.status === 201) { toast({type: 'success', title: 'Shared', description: 'Your share was sent', time: 2000}); } else { const message = await getErrorMessage(response, 'Your share failed to send!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function searchDirectories(path) { const body = {path: path || ''}; const response = await apiPost(`${API_URI}/files/search_directories`, body); if (response.status === 204) { return []; } else if (response.status === 200) { const content = await response.json(); return { is_dir: content.is_dir, directories: content.directories, channel_directories: content.channel_directories, domain_directories: content.domain_directories, } } else { const message = await getErrorMessage(response, 'Failed to search directories!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function renamePath(path, newName) { const body = {path, new_name: newName}; const response = await apiPost(`${API_URI}/files/rename`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to rename!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function movePaths(destination, paths) { const body = {destination, paths}; const response = await apiPost(`${API_URI}/files/move`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to move!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function ignoreDirectory(directory) { const body = {path: directory}; const response = await apiPost(`${API_URI}/files/ignore_directory`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to ignore directory!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function unignoreDirectory(directory) { const body = {path: directory}; const response = await apiPost(`${API_URI}/files/unignore_directory`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to unignore directory!'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function fetchZims() { const response = await apiGet(`${API_URI}/zim`); if (response.ok) { const content = await response.json(); return { zims: content['zims'], } } else { const message = await getErrorMessage(response, 'Cannot fetch Zims'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function fetchZimSubscriptions() { const response = await apiGet(`${API_URI}/zim/subscribe`); if (response.ok) { const content = await response.json(); return { subscriptions: content['subscriptions'], catalog: content['catalog'], iso_639_codes: content['iso_639_codes'], } } else { const message = await getErrorMessage(response, 'Cannot fetch Zim Subscriptions'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function searchZim(offset, limit, searchStr, zimId, activeTags) { offset = parseInt(offset || 0); limit = parseInt(limit || DEFAULT_LIMIT); let body = {offset, limit, search_str: searchStr, tag_names: activeTags || []}; console.debug(`Searching Zim ${zimId} for: ${searchStr} tags: ${activeTags}`); const response = await apiPost(`${ZIM_API}/search/${zimId}`, body); if (response.ok) { const content = await response.json(); const zim = content['zim']; const searchLength = zim['search'].length; console.debug(`Got ${searchLength} results for Zim ${zimId}`); return content['zim']; } } export async function tagZimEntry(zim_id, zim_entry, name) { const body = {tag_name: name, zim_id: zim_id, zim_entry: zim_entry}; const uri = `${API_URI}/zim/tag`; const response = await apiPost(uri, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to add zim tag'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function untagZimEntry(zim_id, zim_entry, name) { const body = {tag_name: name, zim_id: zim_id, zim_entry: zim_entry}; const uri = `${API_URI}/zim/untag`; const response = await apiPost(uri, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to untag zim'); toast({type: 'error', title: 'Error', description: message, time: 5000}); } } export async function zimSubscribe(name, language) { const body = {name, language}; const response = await apiPost(`${API_URI}/zim/subscribe`, body); return response.status === 201; } export async function zimUnsubscribe(id) { const response = await apiDelete(`${API_URI}/zim/subscribe/${id}`); return response.status === 204; } export async function setZimAutoSearch(id, auto_search) { const body = {auto_search}; const response = await apiPost(`${API_URI}/zim/${id}/auto_search`, body); if (response.status !== 204) { throw new Error('Failed to set Zim auto search'); } } export async function fetchDecoded(vinNumber) { const body = {vin_number: vinNumber}; const response = await apiPost(`${API_URI}/vin_number_decoder`, body); if (response.status === 200) { const content = await response.json(); return content['vin']; } else if (response.status === 400) { console.error('Invalid VIN Number'); return null; } } export async function searchSuggestions(search_str) { const body = {search_str}; const response = await apiPost(`${API_URI}/search_suggestions`, body); if (response.ok) { const content = await response.json(); return { channels: content.channels, domains: content.domains, authors: content.authors || [], subjects: content.subjects || [], } } else { console.error('Failed to get file search suggestions!'); } } export async function searchEstimateFiles(search_str, tagNames, mimetypes, months, dateRange, anyTag) { months = months ? months.map(i => parseInt(i)) : []; const body = {search_str, tag_names: tagNames, mimetypes, months, any_tag: anyTag}; if (dateRange) { body['from_year'] = dateRange[0]; body['to_year'] = dateRange[1]; } const response = await apiPost(`${API_URI}/search_file_estimates`, body); if (response.ok) { const content = await response.json(); return { fileGroups: content.file_groups, } } else { console.error('Failed to get file search estimates!'); } } export async function searchEstimateZims(search_str, tagNames) { const body = {search_str: search_str, tag_names: tagNames}; const response = await apiPost(`${API_URI}/zim/search_estimates`, body); if (response.ok) { const content = await response.json(); return { zimsEstimates: content.zims_estimates, } } } export async function searchEstimateOthers(tagNames) { const body = {tag_names: tagNames}; const response = await apiPost(`${API_URI}/search_other_estimates`, body); if (response.ok) { const content = await response.json(); return { others: content.others, } } } export async function searchChannels(tagNames) { const body = {tag_names: tagNames}; const response = await apiPost(`${API_URI}/videos/channels/search`, body); if (response.ok) { const content = await response.json(); return { channels: content.channels, } } } export async function getOutdatedZims() { const response = await apiGet(`${API_URI}/zim/outdated`); if (response.status === 200) { const content = await response.json(); return { outdated: content['outdated'], current: content['current'], } } else { const message = await getErrorMessage(response, 'Cannot fetch outdated Zims. See server logs.'); toast({ type: 'error', title: 'Error', description: message, time: 5000, }); } } export async function deleteOutdatedZims() { const response = await apiDelete(`${API_URI}/zim/outdated`); return response.status === 204; } export async function postShutdown() { // Call Controller directly for system shutdown const {shutdownSystem} = await import('./api/controller'); try { const result = await shutdownSystem(); if (result.success) { return null; // Success } return {code: 'SHUTDOWN_FAILED', error: result.error}; } catch (e) { return {code: 'SHUTDOWN_FAILED', error: e.message}; } } export async function postRestart() { // Call Controller directly for system reboot const {rebootSystem} = await import('./api/controller'); try { const result = await rebootSystem(); if (result.success) { return null; // Success } return {code: 'SHUTDOWN_FAILED', error: result.error}; } catch (e) { return {code: 'SHUTDOWN_FAILED', error: e.message}; } } export async function postVideoFileFormat(video_file_format) { const body = {video_file_format}; return await apiPost(`${API_URI}/videos/file_format`, body); } export async function checkUpgrade(force = false) { const url = force ? `${API_URI}/upgrade/check?force=true` : `${API_URI}/upgrade/check`; const response = await apiGet(url); if (response.ok) { return await response.json(); } return null; } export async function triggerUpgrade() { const response = await apiPost(`${API_URI}/upgrade/start`); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to start upgrade. See server logs.'); toast({ type: 'error', title: 'Upgrade Error', description: message, time: 5000, }); } return response; } export async function getBulkTagPreview(paths) { const body = {paths}; const response = await apiPost(`${API_URI}/files/bulk_tag/preview`, body); if (response.ok) { return await response.json(); } else { const message = await getErrorMessage(response, 'Failed to get bulk tag preview.'); toast({type: 'error', title: 'Bulk Tag Error', description: message, time: 5000}); } } export async function applyBulkTags(paths, addTagNames, removeTagNames) { const body = { paths, add_tag_names: addTagNames || [], remove_tag_names: removeTagNames || [], }; const response = await apiPost(`${API_URI}/files/bulk_tag/apply`, body); if (!response.ok) { const message = await getErrorMessage(response, 'Failed to apply bulk tags.'); toast({type: 'error', title: 'Bulk Tag Error', description: message, time: 5000}); } return response; } export async function getBulkTagProgress() { const response = await apiGet(`${API_URI}/files/bulk_tag/progress`); if (response.ok) { return await response.json(); } // Not toasting because this happens often during polling. return null; } // Encrypted Cookies API export async function getCookiesStatus() { const response = await apiGet(`${VIDEOS_API}/cookies/status`); if (response.ok) { return await response.json(); } const message = await getErrorMessage(response, 'Failed to get cookies status.'); toast({type: 'error', title: 'Cookies Error', description: message, time: 5000}); return null; } export async function uploadCookies(cookiesContent, password) { const body = {cookies_content: cookiesContent, password}; const response = await apiPost(`${VIDEOS_API}/cookies`, body); if (response.ok) { toast({type: 'success', title: 'Cookies Saved', description: 'Cookies encrypted and saved successfully.', time: 3000}); return {success: true}; } const message = await getErrorMessage(response, 'Failed to save cookies.'); toast({type: 'error', title: 'Cookies Error', description: message, time: 5000}); return {success: false, error: message}; } export async function deleteCookies() { const response = await apiDelete(`${VIDEOS_API}/cookies`); if (response.status === 204) { toast({type: 'success', title: 'Cookies Deleted', description: 'Encrypted cookies file deleted.', time: 3000}); return {success: true}; } const message = await getErrorMessage(response, 'Failed to delete cookies.'); toast({type: 'error', title: 'Cookies Error', description: message, time: 5000}); return {success: false, error: message}; } export async function unlockCookies(password) { const body = {password}; const response = await apiPost(`${VIDEOS_API}/cookies/unlock`, body); if (response.ok) { toast({type: 'success', title: 'Cookies Unlocked', description: 'Cookies decrypted and ready for downloads.', time: 3000}); return {success: true}; } const message = await getErrorMessage(response, 'Failed to unlock cookies.'); toast({type: 'error', title: 'Cookies Error', description: message, time: 5000}); return {success: false, error: message}; } export async function lockCookies() { const response = await apiPost(`${VIDEOS_API}/cookies/lock`); if (response.ok) { toast({type: 'success', title: 'Cookies Locked', description: 'Cookies cleared from memory.', time: 3000}); return {success: true}; } const message = await getErrorMessage(response, 'Failed to lock cookies.'); toast({type: 'error', title: 'Cookies Error', description: message, time: 5000}); return {success: false, error: message}; } export async function fetchSuggestedUserAgent() { const response = await apiGet(`${VIDEOS_API}/suggested-user-agent`); if (response.ok) { const data = await response.json(); return data?.user_agent || ''; } return ''; } // Docs API const DOCS_API = '/api/docs'; export async function getDocStatistics() { return await apiGet(`${DOCS_API}/statistics`); } export async function getDoc(fileGroupId) { return await apiGet(`${DOCS_API}/${fileGroupId}`); } export async function searchDocs(offset, limit, searchStr, order, tagNames, author, subject, mimetype) { offset = parseInt(offset || 0); limit = parseInt(limit || DEFAULT_LIMIT); let body = {offset, limit}; if (searchStr) { body['search_str'] = searchStr; } if (order) { body['order_by'] = order; } if (tagNames && tagNames.length > 0) { body['tag_names'] = tagNames; } if (author) { body['author'] = author; } if (subject) { body['subject'] = subject; } if (mimetype) { body['mimetype'] = mimetype; } console.debug('searching docs', body); const response = await apiPost(`${DOCS_API}/search`, body); if (response.ok) { let data = await response.json(); return [data['file_groups'], data['totals']['file_groups']]; } else { const message = await getErrorMessage(response, 'Cannot search docs. See server logs.'); toast({ type: 'error', title: 'Unable to search docs', description: message, time: 5000, }); throw Error(message); } }