mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
* Resume videos and audio where the user left off; show progress on cards Videos and audio resume where the user left off, on the video page and in the file preview, and keep playing. `?t=` starts at an explicit time instead. useFileProgress saves how far the user is through a file: every 15 seconds while it changes, and a final save (a beacon, which survives the page closing) on pause, end, hiding the page, leaving it, or switching files. Each save is also kept in sessionStorage; on open the newer of that and the server's position wins, so coming straight back resumes exactly even if the final save is still in flight. Cards show a progress bar along the bottom of their poster. The dashboard's Recently Viewed Files gains All / Continue; Continue uses the new `in_progress` file search filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Progress review fixes: in_progress totals, latest dashboard request, preview viewed - The in_progress search filter skips the uncounted fast path and is part of the totals cache key, so Continue never reports All's (or the library's) count. - The dashboard's Recently Viewed hook applies only its latest response, and clears the list when switching All/Continue until the new files arrive. - POST /api/files/file returns `viewed`, so a preview can tell a file finished elsewhere from an older position this tab holds. - The progress hook tests restore Blob, fetch and sendBeacon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2630 lines
91 KiB
JavaScript
2630 lines
91 KiB
JavaScript
import {emptyToNull} from "./components/Common";
|
|
import {toast} from "./components/ui";
|
|
import _ from "lodash";
|
|
import {
|
|
API_URI,
|
|
ARCHIVES_API,
|
|
COLLECTIONS_API,
|
|
DEFAULT_LIMIT,
|
|
Downloaders,
|
|
REPOS_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, censored, deep) {
|
|
// 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;
|
|
}
|
|
if (censored) {
|
|
body['censored'] = true;
|
|
}
|
|
if (deep) {
|
|
body['deep'] = 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 updateVideo(fileGroupId, {title, description}) {
|
|
// Edits the Video's details (written to its info json). Only the fields given are changed.
|
|
// Returns the updated video file group.
|
|
const response = await apiPut(`${VIDEOS_API}/${fileGroupId}`, {title, description});
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Unable to save the video. See server logs.');
|
|
toast({type: 'error', title: 'Failed to save', description: message, time: 5000});
|
|
throw new Error(message);
|
|
}
|
|
const data = await response.json();
|
|
return data['file_group'];
|
|
}
|
|
|
|
export async function transcodeVideo(fileGroupId, {
|
|
video_codec = null, audio_codec = null, container = 'mp4', fragmented = false,
|
|
}) {
|
|
// Queues a transcode Job; returns the job id. Watch it with `getJob`.
|
|
const body = {video_codec, audio_codec, container, fragmented};
|
|
const response = await apiPost(`${VIDEOS_API}/${fileGroupId}/transcode`, body);
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Unable to queue the transcode. See server logs.');
|
|
toast({type: 'error', title: 'Failed to transcode', description: message, time: 5000});
|
|
throw new Error(message);
|
|
}
|
|
const data = await response.json();
|
|
return data['job_id'];
|
|
}
|
|
|
|
export async function getJobs() {
|
|
const response = await apiGet(`${API_URI}/jobs`);
|
|
if (!response.ok) {
|
|
throw new Error(await getErrorMessage(response, 'Unable to get the jobs'));
|
|
}
|
|
const data = await response.json();
|
|
return data['jobs'];
|
|
}
|
|
|
|
export async function getJob(jobId) {
|
|
const response = await apiGet(`${API_URI}/jobs/${jobId}`);
|
|
if (!response.ok) {
|
|
const error = new Error(await getErrorMessage(response, 'Unable to get the job'));
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
const data = await response.json();
|
|
return data['job'];
|
|
}
|
|
|
|
export async function cancelJob(jobId) {
|
|
const response = await apiDelete(`${API_URI}/jobs/${jobId}`);
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Unable to cancel the job');
|
|
toast({type: 'error', title: 'Failed to cancel', description: message, time: 5000});
|
|
throw new Error(message);
|
|
}
|
|
const data = await response.json();
|
|
return data['job'];
|
|
}
|
|
|
|
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 getVideoDescription(fileGroupId) {
|
|
const response = await apiGet(`${VIDEOS_API}/${fileGroupId}/description`);
|
|
let data = await response.json();
|
|
return {
|
|
description: data.description,
|
|
}
|
|
}
|
|
|
|
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_daily_limit_global: content.download_daily_limit_global,
|
|
download_daily_limit_per_domain: content.download_daily_limit_per_domain,
|
|
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,
|
|
playlists_destination: content.playlists_destination,
|
|
repos_destination: content.repos_destination,
|
|
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();
|
|
}
|
|
// Undefined, not {dates: []}, so the modal can tell a failed fetch from having no backups.
|
|
}
|
|
|
|
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;
|
|
}
|
|
// Inventory is config-only on the backend (no DB). The frontend loads every inventory once (full), derives
|
|
// list/items/summary/locations client-side, and persists whole inventories on change — so there are only four
|
|
// inventory calls.
|
|
|
|
export async function getInventories() {
|
|
// Returns every inventory in full (fields + items).
|
|
const response = await apiGet(`${API_URI}/inventory`);
|
|
if (response.ok) {
|
|
return (await response.json())['inventories'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Could not get inventories');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function createInventory({name, type}) {
|
|
const response = await apiPost(`${API_URI}/inventory`, {name, type});
|
|
if (response.ok) {
|
|
return (await response.json())['inventory'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to create inventory');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function saveInventory(slug, data) {
|
|
// Whole-inventory save (data = {name?, fields?, items?, version?}). Returns the saved inventory, or null on
|
|
// error/conflict so the caller can reconcile.
|
|
const response = await apiPut(`${API_URI}/inventory/${slug}`, data);
|
|
if (response.ok) {
|
|
return (await response.json())['inventory'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to save inventory');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function deleteInventory(slug) {
|
|
const response = await apiDelete(`${API_URI}/inventory/${slug}`);
|
|
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 reimportInventories() {
|
|
// Re-read every inventory config file from disk (picks up hand-edits and copied-in files).
|
|
const response = await apiPost(`${API_URI}/inventory/reimport`);
|
|
if (response.ok) {
|
|
return (await response.json())['inventories'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to re-import inventories');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function getInventoryBackups(slug) {
|
|
const response = await apiGet(`${API_URI}/inventory/${slug}/backups`);
|
|
if (response.ok) {
|
|
return (await response.json())['dates'];
|
|
}
|
|
// Undefined, not [], so the modal can tell a failed fetch from having no backups.
|
|
}
|
|
|
|
export async function postInventoryRestorePreview(slug, backupDate, mode) {
|
|
const body = {backup_date: backupDate, mode};
|
|
const response = await apiPost(`${API_URI}/inventory/${slug}/restore/preview`, body);
|
|
if (response.ok) {
|
|
return (await response.json())['preview'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to preview the backup');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function postInventoryRestore(slug, backupDate, mode) {
|
|
const body = {backup_date: backupDate, mode};
|
|
const response = await apiPost(`${API_URI}/inventory/${slug}/restore`, body);
|
|
if (response.ok) {
|
|
return (await response.json())['inventory'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to restore the backup');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function getCatalog() {
|
|
// The shared food catalog (known items with total calories per package).
|
|
const response = await apiGet(`${API_URI}/inventory/catalog`);
|
|
if (response.ok) {
|
|
return (await response.json())['catalog'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Could not get the food catalog');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
export async function saveCatalog(items) {
|
|
const response = await apiPut(`${API_URI}/inventory/catalog`, {items});
|
|
if (response.ok) {
|
|
return (await response.json())['catalog'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to save the food catalog');
|
|
toast({type: 'error', title: 'Unexpected server response', description: message, time: 5000});
|
|
return null;
|
|
}
|
|
|
|
|
|
export async function searchArchives(offset, limit, domain, searchStr, order, tagNames, headline, deep) {
|
|
// 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;
|
|
}
|
|
if (deep) {
|
|
body['deep'] = 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);
|
|
}
|
|
}
|
|
|
|
// Tag any Collection (and optionally move its directory).
|
|
export async function tagCollection(collectionId, tagName, directory) {
|
|
const body = {tag_name: tagName};
|
|
if (directory) {
|
|
body['directory'] = directory;
|
|
}
|
|
const response = await apiPost(`${COLLECTIONS_API}/${collectionId}/tag`, body);
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
const message = await getErrorMessage(response, 'Unable to tag. See server logs.');
|
|
toast({type: 'error', title: 'Tag 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() {
|
|
// Not toasting because this will happen often. Undefined on any failure, not an empty list, so
|
|
// the form can say the downloaders could not be fetched.
|
|
try {
|
|
const response = await apiGet(`${API_URI}/downloaders`);
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
} catch (e) {
|
|
// The API is down; reported as undefined below.
|
|
}
|
|
}
|
|
|
|
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, suffix, path, deep, inProgress) {
|
|
const body = {search_str: searchStr, offset: parseInt(offset), limit: parseInt(limit), any_tag: anyTag};
|
|
if (suffix) {
|
|
body['suffix'] = suffix;
|
|
}
|
|
if (path) {
|
|
body['path'] = path;
|
|
}
|
|
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;
|
|
}
|
|
if (deep) {
|
|
body['deep'] = true;
|
|
}
|
|
if (inProgress) {
|
|
body['in_progress'] = true;
|
|
}
|
|
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 flasherSearch(chip, path) {
|
|
// Search .bin firmware, filtered to ESP images for the given chip (from connecting to the device).
|
|
const body = {};
|
|
if (chip) body['chip'] = chip;
|
|
if (path) body['path'] = path;
|
|
const response = await apiPost(`${API_URI}/flasher/search`, body);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return [data['file_groups'], data['totals']['file_groups']];
|
|
}
|
|
const message = await getErrorMessage(response, 'Cannot search firmware. See server logs.');
|
|
toast({type: 'error', title: 'Unable to search firmware', description: message, time: 5000});
|
|
// Undefined, not [null, null], so the picker can tell a failed search from finding nothing.
|
|
}
|
|
|
|
export async function getFlasherConfigs() {
|
|
// Return the user's saved firmware flashing configurations.
|
|
const response = await apiGet(`${API_URI}/flasher/configs`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data['configurations'] || [];
|
|
}
|
|
// Undefined, not [], so the page can tell a failed fetch from an empty list.
|
|
const message = await getErrorMessage(response, 'Could not get saved firmware configurations');
|
|
toast({type: 'error', title: 'Flasher Error', description: message, time: 5000});
|
|
}
|
|
|
|
export async function saveFlasherConfig(name, files, eraseAll) {
|
|
const response = await apiPost(`${API_URI}/flasher/configs`, {name, files, erase_all: eraseAll});
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Cannot save configuration. See server logs.');
|
|
toast({type: 'error', title: 'Unable to save configuration', description: message, time: 5000});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export async function deleteFlasherConfig(name) {
|
|
const response = await apiDelete(`${API_URI}/flasher/configs/${encodeURIComponent(name)}`);
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Cannot delete configuration. See server logs.');
|
|
toast({type: 'error', title: 'Unable to delete configuration', description: message, time: 5000});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
// Undefined, not {results: []}, so a failed search is not shown as finding nothing.
|
|
return undefined;
|
|
}
|
|
return await response.json();
|
|
}
|
|
|
|
export async function getMapSearchStatus() {
|
|
const response = await apiGet(`${API_URI}/map/search/status`);
|
|
if (!response.ok) {
|
|
// Undefined, not {indexed: [], missing: []}, which read as "no maps are installed".
|
|
return undefined;
|
|
}
|
|
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 || [],
|
|
repos: content.repos || [],
|
|
}
|
|
} 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,
|
|
fileGroupsDeep: content.file_groups_deep,
|
|
}
|
|
} 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, searchStr) {
|
|
const body = {tag_names: tagNames, search_str: searchStr || null};
|
|
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, searchStr) {
|
|
const body = {tag_names: tagNames, search_str: searchStr};
|
|
const response = await apiPost(`${API_URI}/videos/channels/search`, body);
|
|
if (response.ok) {
|
|
const content = await response.json();
|
|
return {
|
|
channels: content.channels,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collections of `kind` whose name matches `searchStr` and/or which have any of the tags.
|
|
export async function searchCollections(kind, tagNames, searchStr) {
|
|
const body = {kind, tag_names: tagNames, search_str: searchStr};
|
|
const response = await apiPost(`${COLLECTIONS_API}/search`, body);
|
|
if (response.ok) {
|
|
const content = await response.json();
|
|
return {collections: content.collections};
|
|
}
|
|
}
|
|
|
|
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, deep) {
|
|
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;
|
|
}
|
|
if (deep) {
|
|
body['deep'] = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
export async function fetchPlaylists() {
|
|
const response = await apiGet(`${COLLECTIONS_API}?kind=playlist`);
|
|
if (response.ok) {
|
|
return (await response.json())['collections'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to fetch playlists.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
export async function getPlaylist(playlistId) {
|
|
const response = await apiGet(`${COLLECTIONS_API}/${playlistId}`);
|
|
if (response.ok) {
|
|
return (await response.json())['collection'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to fetch playlist.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
export async function createPlaylist(name, description, tagName) {
|
|
// Trailing slash avoids Sanic's 302 redirect on the root POST route (which would drop the body).
|
|
const response = await apiPost(`${COLLECTIONS_API}/`,
|
|
{name, description, tag_name: tagName || null, kind: 'playlist'});
|
|
if (response.ok) {
|
|
return (await response.json())['collection'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to create playlist.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
export async function deletePlaylist(playlistId) {
|
|
const response = await apiDelete(`${COLLECTIONS_API}/${playlistId}`);
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Failed to delete playlist.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
}
|
|
|
|
export async function addPlaylistItem(playlistId, item) {
|
|
// item: {item_kind, url?, title?, file_group_id?, zim_id?, zim_entry?, position?}
|
|
const response = await apiPost(`${COLLECTIONS_API}/${playlistId}/items`, item);
|
|
if (response.ok) {
|
|
return (await response.json())['item'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to add item.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
export async function removePlaylistItem(playlistId, itemId) {
|
|
const response = await apiDelete(`${COLLECTIONS_API}/${playlistId}/items/${itemId}`);
|
|
if (!response.ok) {
|
|
const message = await getErrorMessage(response, 'Failed to remove item.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
}
|
|
|
|
export async function reorderPlaylistItems(playlistId, itemIds) {
|
|
const response = await apiPut(`${COLLECTIONS_API}/${playlistId}/items/order`, {item_ids: itemIds});
|
|
if (response.ok) {
|
|
return (await response.json())['collection'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to reorder items.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
// Update a playlist's name, description, and/or custom directory ('' clears the directory).
|
|
export async function updatePlaylist(playlistId, {name, description, directory}) {
|
|
const response = await apiPut(`${COLLECTIONS_API}/${playlistId}`, {name, description, directory});
|
|
if (response.ok) {
|
|
return (await response.json())['collection'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to update playlist.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
// Set (tagName) or clear (tagName='') a playlist's single tag. When `directory` is given, the
|
|
// playlist also moves there (the managed tag location is normalized back to auto-managed).
|
|
export async function setPlaylistTag(playlistId, tagName, directory) {
|
|
const body = {tag_name: tagName || ''};
|
|
if (directory !== null && directory !== undefined) {
|
|
body.directory = directory;
|
|
}
|
|
const response = await apiPut(`${COLLECTIONS_API}/${playlistId}`, body);
|
|
if (response.ok) {
|
|
return (await response.json())['collection'];
|
|
}
|
|
const message = await getErrorMessage(response, 'Failed to update playlist tag.');
|
|
toast({type: 'error', title: 'Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
// Bookmarks: a tree of nodes, a node with `children` being a directory.
|
|
export async function getBookmarks() {
|
|
const response = await apiGet(`${API_URI}/bookmarks`);
|
|
if (response.ok) {
|
|
return (await response.json())['bookmarks'];
|
|
}
|
|
throw new Error('Could not fetch bookmarks');
|
|
}
|
|
|
|
async function bookmarksModify(request, fallbackMessage) {
|
|
const response = await request;
|
|
if (response.ok) {
|
|
return response.status === 204 ? null : await response.json();
|
|
}
|
|
const message = await getErrorMessage(response, fallbackMessage);
|
|
toast({type: 'error', title: 'Bookmarks Error', description: message, time: 5000});
|
|
throw new Error(message);
|
|
}
|
|
|
|
export async function addBookmark({name, url, new_tab = false, parent_id = null, position = null}) {
|
|
const body = {name, url, new_tab, parent_id, position};
|
|
return await bookmarksModify(apiPost(`${API_URI}/bookmarks`, body), 'Could not add bookmark');
|
|
}
|
|
|
|
export async function addBookmarkDirectory({name, parent_id = null, position = null}) {
|
|
const body = {name, parent_id, position};
|
|
return await bookmarksModify(apiPost(`${API_URI}/bookmarks/directory`, body), 'Could not add directory');
|
|
}
|
|
|
|
export async function updateBookmark(nodeId, {name, url, new_tab}) {
|
|
const body = {name, url, new_tab};
|
|
return await bookmarksModify(apiPut(`${API_URI}/bookmarks/${nodeId}`, body), 'Could not update bookmark');
|
|
}
|
|
|
|
export async function moveBookmark(nodeId, parent_id, position = null) {
|
|
const body = {parent_id, position};
|
|
return await bookmarksModify(apiPost(`${API_URI}/bookmarks/${nodeId}/move`, body), 'Could not move bookmark');
|
|
}
|
|
|
|
export async function deleteBookmark(nodeId) {
|
|
return await bookmarksModify(apiDelete(`${API_URI}/bookmarks/${nodeId}`), 'Could not delete bookmark');
|
|
}
|
|
|
|
async function reposError(response, fallbackMessage) {
|
|
const message = await getErrorMessage(response, fallbackMessage);
|
|
toast({type: 'error', title: 'Repos Error', description: message, time: 5000});
|
|
throw Error(message);
|
|
}
|
|
|
|
export async function fetchRepos() {
|
|
const response = await apiGet(REPOS_API);
|
|
if (response.ok) {
|
|
return (await response.json())['repos'];
|
|
}
|
|
await reposError(response, 'Failed to fetch repos.');
|
|
}
|
|
|
|
export async function getRepo(repoId) {
|
|
const response = await apiGet(`${REPOS_API}/${repoId}`);
|
|
if (response.ok) {
|
|
return (await response.json())['repo'];
|
|
}
|
|
await reposError(response, 'Failed to fetch repo.');
|
|
}
|
|
|
|
// `repo`: {url, directory?, name?, tag_name?, frequency?, mode?, branch?, description?}
|
|
export async function createRepo(repo) {
|
|
const response = await apiPost(REPOS_API, repo);
|
|
if (response.ok) {
|
|
return (await response.json())['repo'];
|
|
}
|
|
await reposError(response, 'Failed to add repo.');
|
|
}
|
|
|
|
// `changes`: {description?, frequency?, mode?, branch?}. An empty branch follows the remote's default branch.
|
|
export async function updateRepo(repoId, changes) {
|
|
const response = await apiPut(`${REPOS_API}/${repoId}`, changes);
|
|
if (response.ok) {
|
|
return (await response.json())['repo'];
|
|
}
|
|
await reposError(response, 'Failed to update repo.');
|
|
}
|
|
|
|
export async function deleteRepo(repoId, deleteFiles = false) {
|
|
const response = await apiDelete(`${REPOS_API}/${repoId}?delete_files=${deleteFiles ? 'true' : 'false'}`);
|
|
if (!response.ok) {
|
|
await reposError(response, 'Failed to delete repo.');
|
|
}
|
|
}
|
|
|
|
// Download the repo's updates now.
|
|
export async function updateRepoNow(repoId) {
|
|
const response = await apiPost(`${REPOS_API}/${repoId}/update`);
|
|
if (!response.ok) {
|
|
await reposError(response, 'Failed to start the repo update.');
|
|
}
|
|
}
|
|
|
|
// One directory of a repo's files: {path, entries: [{name, path, is_dir, size}], readme_path}.
|
|
export async function getRepoTree(repoId, path) {
|
|
const query = path ? `?path=${encodeURIComponent(path)}` : '';
|
|
const response = await apiGet(`${REPOS_API}/${repoId}/tree${query}`);
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
await reposError(response, 'Failed to list the repo files.');
|
|
}
|
|
|
|
// Repos matching the search (name and README) and/or the tags. Each has an escaped `readme_headline`.
|
|
export async function searchRepos(searchStr, tagNames) {
|
|
const body = {search_str: searchStr || null, tag_names: tagNames || []};
|
|
const response = await apiPost(`${REPOS_API}/search`, body);
|
|
if (response.ok) {
|
|
const content = await response.json();
|
|
return {repos: content.repos, total: content.totals.repos};
|
|
}
|
|
await reposError(response, 'Failed to search repos.');
|
|
}
|
|
|
|
// The commits of a repo's checked out branch, newest first: {commits: [{sha, author, date, message}], total}.
|
|
export async function getRepoLog(repoId, offset = 0, limit = 50) {
|
|
const response = await apiGet(`${REPOS_API}/${repoId}/log?limit=${limit}&offset=${offset}`);
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
await reposError(response, 'Failed to fetch the repo history.');
|
|
}
|
|
|
|
// A ZIP of a repo's checked out files; the browser downloads it.
|
|
export function repoArchiveURL(repoId) {
|
|
return `${REPOS_API}/${repoId}/archive.zip`;
|
|
}
|
|
|
|
// What importing the git clone in `directory` (relative to the media directory) would do. Changes nothing.
|
|
export async function inspectRepoImport(directory, name, tagName, url) {
|
|
const body = {directory, name: name || null, tag_name: tagName || null, url: url || null};
|
|
const response = await apiPost(`${REPOS_API}/import/inspect`, body);
|
|
if (response.ok) {
|
|
return {inspection: (await response.json())['inspection']};
|
|
}
|
|
return {error: await getErrorMessage(response, 'Cannot import this directory.')};
|
|
}
|
|
|
|
// Import an existing git clone as a Repo; it stays where it is. `repo`: {directory, confirm, url?, name?, ...}
|
|
export async function importRepo(repo) {
|
|
const response = await apiPost(`${REPOS_API}/import`, repo);
|
|
if (response.ok) {
|
|
return (await response.json())['repo'];
|
|
}
|
|
await reposError(response, 'Failed to import the repo.');
|
|
}
|