🔀 Merge branch 'dev' into feature/add-basic-authentication
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import { BackendConfigType } from '../../types/config';
|
||||
import { Config } from '../types';
|
||||
import { migrateConfig } from './migrateConfig';
|
||||
|
||||
export function backendMigrateConfig(config: Config, name: string): BackendConfigType {
|
||||
const migratedConfig = migrateConfig(config);
|
||||
|
||||
// Make a backup of the old file ./data/configs/${name}.json
|
||||
// New name is ./data/configs/${name}.bak
|
||||
fs.copyFileSync(`./data/configs/${name}.json`, `./data/configs/${name}.json.bak`);
|
||||
|
||||
// Overrite the file ./data/configs/${name}.json
|
||||
// with the new config format
|
||||
fs.writeFileSync(`./data/configs/${name}.json`, JSON.stringify(migratedConfig, null, 2));
|
||||
|
||||
return migratedConfig;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import Consola from 'consola';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BackendConfigType, ConfigType } from '../../types/config';
|
||||
import { backendMigrateConfig } from './backendMigrateConfig';
|
||||
import { configExists } from './configExists';
|
||||
import { getFallbackConfig } from './getFallbackConfig';
|
||||
import { readConfig } from './readConfig';
|
||||
@@ -16,10 +15,6 @@ export const getConfig = (name: string): BackendConfigType => {
|
||||
// then it is an old config file and we should try to migrate it
|
||||
// to the new format.
|
||||
const config = readConfig(name);
|
||||
if (config.schemaVersion === undefined) {
|
||||
Consola.log('Migrating config file...', config.name);
|
||||
return backendMigrateConfig(config, name);
|
||||
}
|
||||
|
||||
let backendConfig = config as BackendConfigType;
|
||||
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
import Consola from 'consola';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ConfigAppIntegrationType, ConfigAppType, IntegrationType } from '../../types/app';
|
||||
import { AreaType } from '../../types/area';
|
||||
import { CategoryType } from '../../types/category';
|
||||
import { BackendConfigType } from '../../types/config';
|
||||
import { SearchEngineCommonSettingsType } from '../../types/settings';
|
||||
import { ICalendarWidget } from '../../widgets/calendar/CalendarTile';
|
||||
import { IDashDotTile } from '../../widgets/dashDot/DashDotTile';
|
||||
import { IDateWidget } from '../../widgets/date/DateTile';
|
||||
import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetworkTrafficTile';
|
||||
import { ITorrent } from '../../widgets/torrent/TorrentTile';
|
||||
import { IUsenetWidget } from '../../widgets/useNet/UseNetTile';
|
||||
import { IWeatherWidget } from '../../widgets/weather/WeatherTile';
|
||||
import { IWidget } from '../../widgets/widgets';
|
||||
import { Config, serviceItem } from '../types';
|
||||
|
||||
export function migrateConfig(config: Config): BackendConfigType {
|
||||
const newConfig: BackendConfigType = {
|
||||
schemaVersion: 1,
|
||||
configProperties: {
|
||||
name: config.name ?? 'default',
|
||||
},
|
||||
categories: [],
|
||||
widgets: migrateModules(config).filter((widget) => widget !== null),
|
||||
apps: [],
|
||||
settings: {
|
||||
common: {
|
||||
searchEngine: migrateSearchEngine(config),
|
||||
defaultConfig: 'default',
|
||||
},
|
||||
customization: {
|
||||
colors: {
|
||||
primary: config.settings.primaryColor ?? 'red',
|
||||
secondary: config.settings.secondaryColor ?? 'orange',
|
||||
shade: config.settings.primaryShade ?? 7,
|
||||
},
|
||||
layout: {
|
||||
enabledDocker: config.modules.docker?.enabled ?? false,
|
||||
enabledLeftSidebar: false,
|
||||
enabledPing: config.modules.ping?.enabled ?? false,
|
||||
enabledRightSidebar: false,
|
||||
enabledSearchbar: config.modules.search?.enabled ?? true,
|
||||
},
|
||||
accessibility: {
|
||||
disablePingPulse: false,
|
||||
replacePingDotsWithIcons: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wrappers: [
|
||||
{
|
||||
id: 'default',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
config.services.forEach((service) => {
|
||||
const { category: categoryName } = service;
|
||||
|
||||
if (!categoryName) {
|
||||
newConfig.apps.push(
|
||||
migrateService(service, {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const category = getConfigAndCreateIfNotExsists(newConfig, categoryName);
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
newConfig.apps.push(
|
||||
migrateService(service, { type: 'category', properties: { id: category.id } })
|
||||
);
|
||||
});
|
||||
|
||||
Consola.info('Migrator converted a configuration with the old schema to the new schema');
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
const migrateSearchEngine = (config: Config): SearchEngineCommonSettingsType => {
|
||||
switch (config.settings.searchUrl) {
|
||||
case 'https://bing.com/search?q=':
|
||||
return {
|
||||
type: 'bing',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
},
|
||||
};
|
||||
case 'https://google.com/search?q=':
|
||||
return {
|
||||
type: 'google',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
},
|
||||
};
|
||||
case 'https://duckduckgo.com/?q=':
|
||||
return {
|
||||
type: 'duckDuckGo',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: 'custom',
|
||||
properties: {
|
||||
enabled: true,
|
||||
openInNewTab: true,
|
||||
template: config.settings.searchUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getConfigAndCreateIfNotExsists = (
|
||||
config: BackendConfigType,
|
||||
categoryName: string
|
||||
): CategoryType | null => {
|
||||
const foundCategory = config.categories.find((c) => c.name === categoryName);
|
||||
if (foundCategory) {
|
||||
return foundCategory;
|
||||
}
|
||||
|
||||
const category: CategoryType = {
|
||||
id: uuidv4(),
|
||||
name: categoryName,
|
||||
position: config.categories.length + 1, // sync up with index of categories
|
||||
};
|
||||
|
||||
config.categories.push(category);
|
||||
|
||||
// sync up with categories
|
||||
if (config.wrappers.length < config.categories.length) {
|
||||
config.wrappers.push({
|
||||
id: uuidv4(),
|
||||
position: config.wrappers.length + 1, // sync up with index of categories
|
||||
});
|
||||
}
|
||||
|
||||
return category;
|
||||
};
|
||||
|
||||
const migrateService = (oldService: serviceItem, areaType: AreaType): ConfigAppType => ({
|
||||
id: uuidv4(),
|
||||
name: oldService.name,
|
||||
url: oldService.url,
|
||||
behaviour: {
|
||||
isOpeningNewTab: oldService.newTab ?? true,
|
||||
externalUrl: oldService.openedUrl ?? '',
|
||||
},
|
||||
network: {
|
||||
enabledStatusChecker: oldService.ping ?? true,
|
||||
statusCodes: oldService.status ?? ['200'],
|
||||
},
|
||||
appearance: {
|
||||
iconUrl: migrateIcon(oldService.icon),
|
||||
},
|
||||
integration: migrateIntegration(oldService),
|
||||
area: areaType,
|
||||
shape: {},
|
||||
});
|
||||
|
||||
const migrateModules = (config: Config): IWidget<string, any>[] => {
|
||||
const moduleKeys = Object.keys(config.modules);
|
||||
return moduleKeys
|
||||
.map((moduleKey): IWidget<string, any> | null => {
|
||||
const oldModule = config.modules[moduleKey];
|
||||
|
||||
if (!oldModule.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (moduleKey.toLowerCase()) {
|
||||
case 'torrent-status':
|
||||
case 'Torrent':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'torrents-status',
|
||||
properties: {
|
||||
refreshInterval: 10,
|
||||
displayCompletedTorrents: oldModule.options?.hideComplete?.value ?? false,
|
||||
displayStaleTorrents: true,
|
||||
labelFilter: [],
|
||||
labelFilterIsWhitelist: true,
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as ITorrent;
|
||||
case 'weather':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'weather',
|
||||
properties: {
|
||||
displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false,
|
||||
location: {
|
||||
name: oldModule.options?.location?.value ?? '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as IWeatherWidget;
|
||||
case 'dashdot':
|
||||
case 'Dash.': {
|
||||
const oldDashDotService = config.services.find((service) => service.type === 'Dash.');
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'dashdot',
|
||||
properties: {
|
||||
url: oldModule.options?.url?.value ?? oldDashDotService?.url ?? '',
|
||||
cpuMultiView: oldModule.options?.cpuMultiView?.value ?? false,
|
||||
storageMultiView: oldModule.options?.storageMultiView?.value ?? false,
|
||||
useCompactView: oldModule.options?.useCompactView?.value ?? false,
|
||||
graphs: oldModule.options?.graphs?.value ?? ['cpu', 'ram'],
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as unknown as IDashDotTile;
|
||||
}
|
||||
case 'date':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'date',
|
||||
properties: {
|
||||
display24HourFormat: oldModule.options?.full?.value ?? true,
|
||||
},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as IDateWidget;
|
||||
case 'Download Speed' || 'dlspeed':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'dlspeed',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as ITorrentNetworkTraffic;
|
||||
case 'calendar':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'calendar',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as ICalendarWidget;
|
||||
case 'usenet':
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type: 'usenet',
|
||||
properties: {},
|
||||
area: {
|
||||
type: 'wrapper',
|
||||
properties: {
|
||||
id: 'default',
|
||||
},
|
||||
},
|
||||
shape: {},
|
||||
} as IUsenetWidget;
|
||||
default:
|
||||
Consola.error(`Failed to map unknown module type ${moduleKey} to new type definitions.`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((x) => x !== null) as IWidget<string, any>[];
|
||||
};
|
||||
|
||||
const migrateIcon = (iconUrl: string) => {
|
||||
if (iconUrl.startsWith('https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/')) {
|
||||
const icon = iconUrl.split('/').at(-1);
|
||||
Consola.warn(
|
||||
`Detected legacy icon repository. Upgrading to replacement repository: ${iconUrl} -> ${icon}`
|
||||
);
|
||||
return `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${icon}`;
|
||||
}
|
||||
|
||||
return iconUrl;
|
||||
};
|
||||
|
||||
const migrateIntegration = (oldService: serviceItem): ConfigAppIntegrationType => {
|
||||
const logInformation = (newType: IntegrationType) => {
|
||||
Consola.info(`Migrated integration ${oldService.type} to the new type ${newType}`);
|
||||
};
|
||||
switch (oldService.type) {
|
||||
case 'Deluge':
|
||||
logInformation('deluge');
|
||||
return {
|
||||
type: 'deluge',
|
||||
properties: [
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: oldService.password,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Jellyseerr':
|
||||
logInformation('jellyseerr');
|
||||
return {
|
||||
type: 'jellyseerr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Overseerr':
|
||||
logInformation('overseerr');
|
||||
return {
|
||||
type: 'overseerr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Lidarr':
|
||||
logInformation('lidarr');
|
||||
return {
|
||||
type: 'lidarr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Radarr':
|
||||
logInformation('radarr');
|
||||
return {
|
||||
type: 'radarr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Readarr':
|
||||
logInformation('readarr');
|
||||
return {
|
||||
type: 'readarr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Sabnzbd':
|
||||
logInformation('sabnzbd');
|
||||
return {
|
||||
type: 'sabnzbd',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Sonarr':
|
||||
logInformation('sonarr');
|
||||
return {
|
||||
type: 'sonarr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: oldService.apiKey,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'NZBGet':
|
||||
logInformation('nzbGet');
|
||||
return {
|
||||
type: 'nzbGet',
|
||||
properties: [
|
||||
{
|
||||
field: 'username',
|
||||
type: 'private',
|
||||
value: oldService.username,
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: oldService.password,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'qBittorrent':
|
||||
logInformation('qBittorrent');
|
||||
return {
|
||||
type: 'qBittorrent',
|
||||
properties: [
|
||||
{
|
||||
field: 'username',
|
||||
type: 'private',
|
||||
value: oldService.username,
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: oldService.password,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Transmission':
|
||||
logInformation('transmission');
|
||||
return {
|
||||
type: 'transmission',
|
||||
properties: [
|
||||
{
|
||||
field: 'username',
|
||||
type: 'private',
|
||||
value: oldService.username,
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
type: 'private',
|
||||
value: oldService.password,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'Other':
|
||||
return {
|
||||
type: null,
|
||||
properties: [],
|
||||
};
|
||||
default:
|
||||
Consola.warn(
|
||||
`Integration type of service ${oldService.name} could not be mapped to new integration type definition`
|
||||
);
|
||||
return {
|
||||
type: null,
|
||||
properties: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { ConfigType } from '../types/config';
|
||||
import { getFallbackConfig } from './config/getFallbackConfig';
|
||||
|
||||
export function getConfig(name: string, props: any = undefined) {
|
||||
// Check if the config file exists
|
||||
const configPath = path.join(process.cwd(), 'data/configs', `${name}.json`);
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return getFallbackConfig() as unknown as ConfigType;
|
||||
}
|
||||
const config = fs.readFileSync(configPath, 'utf8');
|
||||
// Print loaded config
|
||||
return {
|
||||
props: {
|
||||
configName: name,
|
||||
config: JSON.parse(config),
|
||||
...props,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
import { MantineTheme } from '@mantine/core';
|
||||
|
||||
import { OptionValues } from '../modules/ModuleTypes';
|
||||
|
||||
export interface Settings {
|
||||
searchUrl: string;
|
||||
searchNewTab?: boolean;
|
||||
title?: string;
|
||||
logo?: string;
|
||||
favicon?: string;
|
||||
primaryColor?: MantineTheme['primaryColor'];
|
||||
secondaryColor?: MantineTheme['primaryColor'];
|
||||
primaryShade?: MantineTheme['primaryShade'];
|
||||
background?: string;
|
||||
customCSS?: string;
|
||||
appOpacity?: number;
|
||||
widgetPosition?: string;
|
||||
grow?: boolean;
|
||||
appCardWidth?: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
name: string;
|
||||
services: serviceItem[];
|
||||
settings: Settings;
|
||||
modules: {
|
||||
[key: string]: ConfigModule;
|
||||
};
|
||||
}
|
||||
|
||||
interface ConfigModule {
|
||||
title: string;
|
||||
enabled: boolean;
|
||||
options: {
|
||||
[key: string]: OptionValues;
|
||||
};
|
||||
}
|
||||
|
||||
export const Targets = [
|
||||
{ value: '_blank', label: 'New Tab' },
|
||||
{ value: '_top', label: 'Same Window' },
|
||||
];
|
||||
|
||||
export const ServiceTypeList = [
|
||||
'Other',
|
||||
'Dash.',
|
||||
'Deluge',
|
||||
'Emby',
|
||||
'Lidarr',
|
||||
'Plex',
|
||||
'qBittorrent',
|
||||
'Radarr',
|
||||
'Readarr',
|
||||
'Sonarr',
|
||||
'Transmission',
|
||||
'Overseerr',
|
||||
'Jellyseerr',
|
||||
'Sabnzbd',
|
||||
'NZBGet',
|
||||
];
|
||||
export type ServiceType =
|
||||
| 'Other'
|
||||
| 'Dash.'
|
||||
| 'Deluge'
|
||||
| 'Emby'
|
||||
| 'Lidarr'
|
||||
| 'Plex'
|
||||
| 'qBittorrent'
|
||||
| 'Radarr'
|
||||
| 'Readarr'
|
||||
| 'Sonarr'
|
||||
| 'Overseerr'
|
||||
| 'Jellyseerr'
|
||||
| 'Transmission'
|
||||
| 'Sabnzbd'
|
||||
| 'NZBGet';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param name the name to match
|
||||
* @param form the form
|
||||
* @returns the port from the map
|
||||
*/
|
||||
export function tryMatchPort(name: string | undefined, form?: any) {
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
// Match name with portmap key
|
||||
const port = portmap.find((p) => p.name === name.toLowerCase());
|
||||
if (form && port) {
|
||||
form.setFieldValue('url', `http://localhost:${port.value}`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
export const portmap = [
|
||||
{ name: 'qbittorrent', value: '8080' },
|
||||
{ name: 'sonarr', value: '8989' },
|
||||
{ name: 'radarr', value: '7878' },
|
||||
{ name: 'lidarr', value: '8686' },
|
||||
{ name: 'readarr', value: '8787' },
|
||||
{ name: 'deluge', value: '8112' },
|
||||
{ name: 'transmission', value: '9091' },
|
||||
{ name: 'plex', value: '32400' },
|
||||
{ name: 'emby', value: '8096' },
|
||||
{ name: 'overseerr', value: '5055' },
|
||||
{ name: 'dash.', value: '3001' },
|
||||
{ name: 'sabnzbd', value: '8080' },
|
||||
{ name: 'nzbget', value: '6789' },
|
||||
];
|
||||
|
||||
//TODO: Fix this to be used in the docker add to homarr button
|
||||
export const MatchingImages: {
|
||||
image: string;
|
||||
type: ServiceType;
|
||||
}[] = [
|
||||
//Official images
|
||||
{ image: 'mauricenino/dashdot', type: 'Dash.' },
|
||||
{ image: 'emby/embyserver', type: 'Emby' },
|
||||
{ image: 'plexinc/pms-docker', type: 'Plex' },
|
||||
//Lidarr images
|
||||
{ image: 'hotio/lidarr', type: 'Lidarr' },
|
||||
{ image: 'ghcr.io/hotio/lidarr', type: 'Lidarr' },
|
||||
{ image: 'cr.hotio.dev/hotio/lidarr', type: 'Lidarr' },
|
||||
// Plex
|
||||
{ image: 'hotio/plex', type: 'Plex' },
|
||||
{ image: 'ghcr.io/hotio/plex', type: 'Plex' },
|
||||
{ image: 'cr.hotio.dev/hotio/plex', type: 'Plex' },
|
||||
// qbittorrent
|
||||
{ image: 'hotio/qbittorrent', type: 'qBittorrent' },
|
||||
{ image: 'ghcr.io/hotio/qbittorrent', type: 'qBittorrent' },
|
||||
{ image: 'cr.hotio.dev/hotio/qbittorrent', type: 'qBittorrent' },
|
||||
// Radarr
|
||||
{ image: 'hotio/radarr', type: 'Radarr' },
|
||||
{ image: 'ghcr.io/hotio/radarr', type: 'Radarr' },
|
||||
{ image: 'cr.hotio.dev/hotio/radarr', type: 'Radarr' },
|
||||
// Readarr
|
||||
{ image: 'hotio/readarr', type: 'Readarr' },
|
||||
{ image: 'ghcr.io/hotio/readarr', type: 'Readarr' },
|
||||
{ image: 'cr.hotio.dev/hotio/readarr', type: 'Readarr' },
|
||||
// Sonarr
|
||||
{ image: 'hotio/sonarr', type: 'Sonarr' },
|
||||
{ image: 'ghcr.io/hotio/sonarr', type: 'Sonarr' },
|
||||
{ image: 'cr.hotio.dev/hotio/sonarr', type: 'Sonarr' },
|
||||
//LinuxServer images
|
||||
{ image: 'lscr.io/linuxserver/deluge', type: 'Deluge' },
|
||||
{ image: 'lscr.io/linuxserver/emby', type: 'Emby' },
|
||||
{ image: 'lscr.io/linuxserver/lidarr', type: 'Lidarr' },
|
||||
{ image: 'lscr.io/linuxserver/plex', type: 'Plex' },
|
||||
{ image: 'lscr.io/linuxserver/qbittorrent', type: 'qBittorrent' },
|
||||
{ image: 'lscr.io/linuxserver/radarr', type: 'Radarr' },
|
||||
{ image: 'lscr.io/linuxserver/readarr', type: 'Readarr' },
|
||||
{ image: 'lscr.io/linuxserver/sonarr', type: 'Sonarr' },
|
||||
{ image: 'lscr.io/linuxserver/transmission', type: 'Transmission' },
|
||||
// LinuxServer but on Docker Hub
|
||||
{ image: 'linuxserver/deluge', type: 'Deluge' },
|
||||
{ image: 'linuxserver/emby', type: 'Emby' },
|
||||
{ image: 'linuxserver/lidarr', type: 'Lidarr' },
|
||||
{ image: 'linuxserver/plex', type: 'Plex' },
|
||||
{ image: 'linuxserver/qbittorrent', type: 'qBittorrent' },
|
||||
{ image: 'linuxserver/radarr', type: 'Radarr' },
|
||||
{ image: 'linuxserver/readarr', type: 'Readarr' },
|
||||
{ image: 'linuxserver/sonarr', type: 'Sonarr' },
|
||||
{ image: 'linuxserver/transmission', type: 'Transmission' },
|
||||
//High usage
|
||||
{ image: 'markusmcnugen/qbittorrentvpn', type: 'qBittorrent' },
|
||||
{ image: 'haugene/transmission-openvpn', type: 'Transmission' },
|
||||
];
|
||||
|
||||
export interface serviceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ServiceType;
|
||||
url: string;
|
||||
icon: string;
|
||||
category?: string;
|
||||
apiKey?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
openedUrl?: string;
|
||||
newTab?: boolean;
|
||||
ping?: boolean;
|
||||
status?: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user