mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(cache): add cache table and flush cache option to settings
also increases tmdb cache times to about 6 hours (12 hours for detail requests)
This commit is contained in:
@@ -2024,6 +2024,56 @@ paths:
|
||||
running:
|
||||
type: boolean
|
||||
example: false
|
||||
/settings/cache:
|
||||
get:
|
||||
summary: Get a list of active caches
|
||||
description: Retrieves a list of all active caches and their current stats.
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: Caches returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: cache-id
|
||||
name:
|
||||
type: string
|
||||
example: cache name
|
||||
stats:
|
||||
type: object
|
||||
properties:
|
||||
hits:
|
||||
type: number
|
||||
misses:
|
||||
type: number
|
||||
keys:
|
||||
type: number
|
||||
ksize:
|
||||
type: number
|
||||
vsize:
|
||||
type: number
|
||||
/settings/cache/{cacheId}/flush:
|
||||
get:
|
||||
summary: Flush a specific cache
|
||||
description: Flushes all data from the cache ID provided
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: path
|
||||
name: cacheId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: 'Flushed cache'
|
||||
/settings/notifications:
|
||||
get:
|
||||
summary: Return notification settings
|
||||
|
@@ -148,7 +148,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
append_to_response: 'credits,external_ids,videos',
|
||||
},
|
||||
},
|
||||
900
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
@@ -174,7 +174,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
'aggregate_credits,credits,external_ids,keywords,videos',
|
||||
},
|
||||
},
|
||||
900
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
|
@@ -11,3 +11,15 @@ export interface PublicSettingsResponse {
|
||||
series4kEnabled: boolean;
|
||||
hideAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface CacheItem {
|
||||
id: string;
|
||||
name: string;
|
||||
stats: {
|
||||
hits: number;
|
||||
misses: number;
|
||||
keys: number;
|
||||
ksize: number;
|
||||
vsize: number;
|
||||
};
|
||||
}
|
||||
|
@@ -1,45 +1,49 @@
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
|
||||
|
||||
interface Cache {
|
||||
id: AvailableCacheIds;
|
||||
data: NodeCache;
|
||||
}
|
||||
export type AvailableCacheIds = 'tmdb' | 'radarr' | 'sonarr' | 'rt';
|
||||
|
||||
const DEFAULT_TTL = 300;
|
||||
const DEFAULT_CHECK_PERIOD = 120;
|
||||
|
||||
class Cache {
|
||||
public id: AvailableCacheIds;
|
||||
public data: NodeCache;
|
||||
public name: string;
|
||||
|
||||
constructor(
|
||||
id: AvailableCacheIds,
|
||||
name: string,
|
||||
options: { stdTtl?: number; checkPeriod?: number } = {}
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.data = new NodeCache({
|
||||
stdTTL: options.stdTtl ?? DEFAULT_TTL,
|
||||
checkperiod: options.checkPeriod ?? DEFAULT_CHECK_PERIOD,
|
||||
});
|
||||
}
|
||||
|
||||
public getStats() {
|
||||
return this.data.getStats();
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this.data.flushAll();
|
||||
}
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
private availableCaches: Record<AvailableCacheIds, Cache> = {
|
||||
tmdb: {
|
||||
id: 'tmdb',
|
||||
data: new NodeCache({
|
||||
stdTTL: DEFAULT_TTL,
|
||||
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||
}),
|
||||
},
|
||||
radarr: {
|
||||
id: 'radarr',
|
||||
data: new NodeCache({
|
||||
stdTTL: DEFAULT_TTL,
|
||||
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||
}),
|
||||
},
|
||||
sonarr: {
|
||||
id: 'sonarr',
|
||||
data: new NodeCache({
|
||||
stdTTL: DEFAULT_TTL,
|
||||
checkperiod: DEFAULT_CHECK_PERIOD,
|
||||
}),
|
||||
},
|
||||
rt: {
|
||||
id: 'rt',
|
||||
data: new NodeCache({
|
||||
stdTTL: 21600, // 12 hours TTL
|
||||
checkperiod: 60 * 30, // 30 minutes check period
|
||||
}),
|
||||
},
|
||||
tmdb: new Cache('tmdb', 'TMDb API', {
|
||||
stdTtl: 21600,
|
||||
checkPeriod: 60 * 30,
|
||||
}),
|
||||
radarr: new Cache('radarr', 'Radarr API'),
|
||||
sonarr: new Cache('sonarr', 'Sonarr API'),
|
||||
rt: new Cache('rt', 'Rotten Tomatoes API', {
|
||||
stdTtl: 43200,
|
||||
checkPeriod: 60 * 30,
|
||||
}),
|
||||
};
|
||||
|
||||
public getCache(id: AvailableCacheIds): Cache {
|
||||
|
@@ -16,6 +16,7 @@ import { SettingsAboutResponse } from '../../interfaces/api/settingsInterfaces';
|
||||
import notificationRoutes from './notifications';
|
||||
import sonarrRoutes from './sonarr';
|
||||
import radarrRoutes from './radarr';
|
||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
||||
|
||||
const settingsRoutes = Router();
|
||||
|
||||
@@ -273,6 +274,32 @@ settingsRoutes.get<{ jobId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/cache', (req, res) => {
|
||||
const caches = cacheManager.getAllCaches();
|
||||
|
||||
return res.status(200).json(
|
||||
Object.values(caches).map((cache) => ({
|
||||
id: cache.id,
|
||||
name: cache.name,
|
||||
stats: cache.getStats(),
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
settingsRoutes.get<{ cacheId: AvailableCacheIds }>(
|
||||
'/cache/:cacheId/flush',
|
||||
(req, res, next) => {
|
||||
const cache = cacheManager.getCache(req.params.cacheId);
|
||||
|
||||
if (cache) {
|
||||
cache.flush();
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
next({ status: 404, message: 'Cache does not exist.' });
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get(
|
||||
'/initialize',
|
||||
isAuthenticated(Permission.ADMIN),
|
||||
|
@@ -7,18 +7,7 @@ import type {
|
||||
ServiceCommonServerWithDetails,
|
||||
} from '../../../../server/interfaces/api/serviceInterfaces';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
import { formatBytes } from '../../../utils/numberHelpers';
|
||||
|
||||
const messages = defineMessages({
|
||||
advancedoptions: 'Advanced Options',
|
||||
|
@@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import useSWR from 'swr';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
|
||||
import Button from '../Common/Button';
|
||||
import Table from '../Common/Table';
|
||||
import Spinner from '../../assets/spinner.svg';
|
||||
import axios from 'axios';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import Badge from '../Common/Badge';
|
||||
|
||||
const messages = defineMessages({
|
||||
jobname: 'Job Name',
|
||||
jobtype: 'Type',
|
||||
nextexecution: 'Next Execution',
|
||||
runnow: 'Run Now',
|
||||
canceljob: 'Cancel Job',
|
||||
jobstarted: '{jobname} started.',
|
||||
jobcancelled: '{jobname} cancelled.',
|
||||
});
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
const SettingsJobs: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<Job[]>('/api/v1/settings/jobs', {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const runJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
|
||||
addToast(
|
||||
intl.formatMessage(messages.jobstarted, {
|
||||
jobname: job.name,
|
||||
}),
|
||||
{
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
}
|
||||
);
|
||||
revalidate();
|
||||
};
|
||||
|
||||
const cancelJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
|
||||
addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
revalidate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.jobtype)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data?.map((job) => (
|
||||
<tr key={`job-list-${job.id}`}>
|
||||
<Table.TD>
|
||||
<div className="flex items-center text-sm leading-5 text-white">
|
||||
{job.running && <Spinner className="w-5 h-5 mr-2" />}
|
||||
<span>{job.name}</span>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<Badge
|
||||
badgeType={job.type === 'process' ? 'primary' : 'warning'}
|
||||
className="uppercase"
|
||||
>
|
||||
{job.type}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="text-sm leading-5 text-white">
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(job.nextExecutionTime).getTime() - Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
{job.running ? (
|
||||
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
||||
{intl.formatMessage(messages.canceljob)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button buttonType="primary" onClick={() => runJob(job)}>
|
||||
{intl.formatMessage(messages.runnow)}
|
||||
</Button>
|
||||
)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
))}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsJobs;
|
198
src/components/Settings/SettingsJobsCache/index.tsx
Normal file
198
src/components/Settings/SettingsJobsCache/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import useSWR from 'swr';
|
||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
|
||||
import Button from '../../Common/Button';
|
||||
import Table from '../../Common/Table';
|
||||
import Spinner from '../../../assets/spinner.svg';
|
||||
import axios from 'axios';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import Badge from '../../Common/Badge';
|
||||
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||
import { formatBytes } from '../../../utils/numberHelpers';
|
||||
|
||||
const messages = defineMessages({
|
||||
jobs: 'Jobs',
|
||||
jobsDescription:
|
||||
'Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
|
||||
jobname: 'Job Name',
|
||||
jobtype: 'Type',
|
||||
nextexecution: 'Next Execution',
|
||||
runnow: 'Run Now',
|
||||
canceljob: 'Cancel Job',
|
||||
jobstarted: '{jobname} started.',
|
||||
jobcancelled: '{jobname} cancelled.',
|
||||
cache: 'Cache',
|
||||
cacheDescription:
|
||||
'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
|
||||
cacheflushed: '{cachename} cache flushed.',
|
||||
cachename: 'Cache Name',
|
||||
cachehits: 'Hits',
|
||||
cachemisses: 'Misses',
|
||||
cachekeys: 'Total Keys',
|
||||
cacheksize: 'Key Size',
|
||||
cachevsize: 'Value Size',
|
||||
flushcache: 'Flush Cache',
|
||||
});
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
nextExecutionTime: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
const SettingsJobs: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const { data, error, revalidate } = useSWR<Job[]>('/api/v1/settings/jobs', {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { data: cacheData, revalidate: cacheRevalidate } = useSWR<CacheItem[]>(
|
||||
'/api/v1/settings/cache',
|
||||
{
|
||||
refreshInterval: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const runJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
|
||||
addToast(
|
||||
intl.formatMessage(messages.jobstarted, {
|
||||
jobname: job.name,
|
||||
}),
|
||||
{
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
}
|
||||
);
|
||||
revalidate();
|
||||
};
|
||||
|
||||
const cancelJob = async (job: Job) => {
|
||||
await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
|
||||
addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
revalidate();
|
||||
};
|
||||
|
||||
const flushCache = async (cache: CacheItem) => {
|
||||
await axios.get(`/api/v1/settings/cache/${cache.id}/flush`);
|
||||
addToast(
|
||||
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
|
||||
{
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
}
|
||||
);
|
||||
cacheRevalidate();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-200">
|
||||
{intl.formatMessage(messages.jobs)}
|
||||
</h3>
|
||||
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
||||
{intl.formatMessage(messages.jobsDescription)}
|
||||
</p>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.jobtype)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data?.map((job) => (
|
||||
<tr key={`job-list-${job.id}`}>
|
||||
<Table.TD>
|
||||
<div className="flex items-center text-sm leading-5 text-white">
|
||||
{job.running && <Spinner className="w-5 h-5 mr-2" />}
|
||||
<span>{job.name}</span>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<Badge
|
||||
badgeType={job.type === 'process' ? 'primary' : 'warning'}
|
||||
className="uppercase"
|
||||
>
|
||||
{job.type}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<div className="text-sm leading-5 text-white">
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(job.nextExecutionTime).getTime() - Date.now()) /
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
{job.running ? (
|
||||
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
||||
{intl.formatMessage(messages.canceljob)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button buttonType="primary" onClick={() => runJob(job)}>
|
||||
{intl.formatMessage(messages.runnow)}
|
||||
</Button>
|
||||
)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
))}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
<div className="my-4">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-200">
|
||||
{intl.formatMessage(messages.cache)}
|
||||
</h3>
|
||||
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
|
||||
{intl.formatMessage(messages.cacheDescription)}
|
||||
</p>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cachehits)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cachemisses)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cachekeys)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cacheksize)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cachevsize)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{cacheData?.map((cache) => (
|
||||
<tr key={`cache-list-${cache.id}`}>
|
||||
<Table.TD>{cache.name}</Table.TD>
|
||||
<Table.TD>{cache.stats.hits}</Table.TD>
|
||||
<Table.TD>{cache.stats.misses}</Table.TD>
|
||||
<Table.TD>{cache.stats.keys}</Table.TD>
|
||||
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
|
||||
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
|
||||
<Table.TD alignText="right">
|
||||
<Button buttonType="danger" onClick={() => flushCache(cache)}>
|
||||
{intl.formatMessage(messages.flushcache)}
|
||||
</Button>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
))}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsJobs;
|
@@ -9,7 +9,7 @@ const messages = defineMessages({
|
||||
menuServices: 'Services',
|
||||
menuNotifications: 'Notifications',
|
||||
menuLogs: 'Logs',
|
||||
menuJobs: 'Jobs',
|
||||
menuJobs: 'Jobs & Cache',
|
||||
menuAbout: 'About',
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ const SettingsLayout: React.FC = ({ children }) => {
|
||||
)?.route
|
||||
}
|
||||
aria-label="Selected tab"
|
||||
className="bg-gray-800 text-white mt-1 rounded-md form-select block w-full pl-3 pr-10 py-2 text-base leading-6 border-gray-700 focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5 transition ease-in-out duration-150"
|
||||
className="block w-full py-2 pl-3 pr-10 mt-1 text-base leading-6 text-white transition duration-150 ease-in-out bg-gray-800 border-gray-700 rounded-md form-select focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5"
|
||||
>
|
||||
{settingsRoutes.map((route, index) => (
|
||||
<SettingsLink
|
||||
@@ -122,7 +122,7 @@ const SettingsLayout: React.FC = ({ children }) => {
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="border-b border-gray-600">
|
||||
<nav className="-mb-px flex">
|
||||
<nav className="flex -mb-px">
|
||||
{settingsRoutes.map((route, index) => (
|
||||
<SettingsLink
|
||||
route={route.route}
|
||||
|
@@ -345,6 +345,25 @@
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
"components.Settings.SettingsAbout.version": "Version",
|
||||
"components.Settings.SettingsJobsCache.cache": "Cache",
|
||||
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
|
||||
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
|
||||
"components.Settings.SettingsJobsCache.cachehits": "Hits",
|
||||
"components.Settings.SettingsJobsCache.cachekeys": "Total Keys",
|
||||
"components.Settings.SettingsJobsCache.cacheksize": "Key Size",
|
||||
"components.Settings.SettingsJobsCache.cachemisses": "Misses",
|
||||
"components.Settings.SettingsJobsCache.cachename": "Cache Name",
|
||||
"components.Settings.SettingsJobsCache.cachevsize": "Value Size",
|
||||
"components.Settings.SettingsJobsCache.canceljob": "Cancel Job",
|
||||
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} cancelled.",
|
||||
"components.Settings.SettingsJobsCache.jobname": "Job Name",
|
||||
"components.Settings.SettingsJobsCache.jobs": "Jobs",
|
||||
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.",
|
||||
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.",
|
||||
"components.Settings.SettingsJobsCache.jobtype": "Type",
|
||||
"components.Settings.SettingsJobsCache.nextexecution": "Next Execution",
|
||||
"components.Settings.SettingsJobsCache.runnow": "Run Now",
|
||||
"components.Settings.SonarrModal.add": "Add Server",
|
||||
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
|
||||
"components.Settings.SonarrModal.animerootfolder": "Anime Root Folder",
|
||||
@@ -393,7 +412,6 @@
|
||||
"components.Settings.apikey": "API Key",
|
||||
"components.Settings.applicationurl": "Application URL",
|
||||
"components.Settings.autoapprovedrequests": "Send Notifications for Auto-Approved Requests",
|
||||
"components.Settings.canceljob": "Cancel Job",
|
||||
"components.Settings.cancelscan": "Cancel Scan",
|
||||
"components.Settings.copied": "Copied API key to clipboard.",
|
||||
"components.Settings.csrfProtection": "Enable CSRF Protection",
|
||||
@@ -410,22 +428,17 @@
|
||||
"components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.",
|
||||
"components.Settings.hideAvailable": "Hide Available Media",
|
||||
"components.Settings.hostname": "Hostname/IP",
|
||||
"components.Settings.jobcancelled": "{jobname} cancelled.",
|
||||
"components.Settings.jobname": "Job Name",
|
||||
"components.Settings.jobstarted": "{jobname} started.",
|
||||
"components.Settings.jobtype": "Type",
|
||||
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||
"components.Settings.manualscan": "Manual Library Scan",
|
||||
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
"components.Settings.menuAbout": "About",
|
||||
"components.Settings.menuGeneralSettings": "General Settings",
|
||||
"components.Settings.menuJobs": "Jobs",
|
||||
"components.Settings.menuJobs": "Jobs & Cache",
|
||||
"components.Settings.menuLogs": "Logs",
|
||||
"components.Settings.menuNotifications": "Notifications",
|
||||
"components.Settings.menuPlexSettings": "Plex",
|
||||
"components.Settings.menuServices": "Services",
|
||||
"components.Settings.ms": "ms",
|
||||
"components.Settings.nextexecution": "Next Execution",
|
||||
"components.Settings.nodefault": "No default server selected!",
|
||||
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
|
||||
"components.Settings.notificationAgentSettingsDescription": "Choose the types of notifications to send, and which notification agents to use.",
|
||||
@@ -442,7 +455,6 @@
|
||||
"components.Settings.port": "Port",
|
||||
"components.Settings.radarrSettingsDescription": "Set up your Radarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD, and one for 4K). Administrators can override the server is used for new requests.",
|
||||
"components.Settings.radarrsettings": "Radarr Settings",
|
||||
"components.Settings.runnow": "Run Now",
|
||||
"components.Settings.save": "Save Changes",
|
||||
"components.Settings.saving": "Saving…",
|
||||
"components.Settings.serverConnected": "connected",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import SettingsLayout from '../../components/Settings/SettingsLayout';
|
||||
import SettingsJobs from '../../components/Settings/SettingsJobs';
|
||||
import SettingsJobs from '../../components/Settings/SettingsJobsCache';
|
||||
import { Permission } from '../../hooks/useUser';
|
||||
import useRouteGuard from '../../hooks/useRouteGuard';
|
||||
|
||||
|
11
src/utils/numberHelpers.ts
Normal file
11
src/utils/numberHelpers.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
Reference in New Issue
Block a user