feat: move config validator into addon package and also check config at each request

This commit is contained in:
Viren070
2024-12-26 19:13:58 +00:00
parent 29ef6a738a
commit 27b2ef9bd6
7 changed files with 339 additions and 382 deletions
-15
View File
@@ -9,21 +9,6 @@ export class AIOStreams {
this.config = config;
}
public configValidator() {
if (!this.config) {
throw new Error('No config provided');
}
if (!this.config.resolutions) {
throw new Error('No resolutions provided');
}
if (!this.config.qualities) {
throw new Error('No qualities provided');
}
if (!this.config.visualTags) {
throw new Error('No visualTags provided');
}
}
public async getStreams(streamRequest: StreamRequest): Promise<Stream[]> {
const streams: Stream[] = [];
+282
View File
@@ -0,0 +1,282 @@
import { Config } from "@aiostreams/types";
import { AddonDetail } from "@aiostreams/types";
export const allowedFormatters = ['gdrive', 'torrentio', 'torbox'];
export const addonDetails: AddonDetail[] = [
{
name: 'Torrentio',
id: 'torrentio',
options: [
{
id: 'overrideUrl',
required: false,
label: 'Override URL',
description:
'Override the URL used to fetch streams from the torrentio addon',
type: 'text',
},
{
id: 'useMultipleInstances',
required: false,
label: 'Use Multiple Instances',
description:
'Use multiple instances of the torrentio addon to fetch streams when using multiple services',
type: 'checkbox',
},
],
},
{
name: 'Torbox',
id: 'torbox',
},
{
name: 'Google Drive (Viren070)',
id: 'gdrive',
options: [
{
id: 'addonUrl',
required: true,
label: 'Addon URL',
description: 'The URL of the Google Drive addon',
type: 'text',
},
],
},
{
name: 'Custom',
id: 'custom',
options: [
{
id: 'url',
required: true,
description: 'The URL of the custom addon',
label: 'URL',
type: 'text',
},
{
id: 'name',
required: true,
description: 'The name of the custom addon',
label: 'Name',
type: 'text',
}
],
},
];
export const allowedLanguages = ['English', 'Spanish', 'French', 'German', 'Chinese'];
export const serviceCredentials = [
{
name: 'Real Debrid',
id: 'realdebrid',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://real-debrid.com/apitoken',
}
]
},
{
name: 'All Debrid',
id: 'alldebrid',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://alldebrid.com/apikeys',
}
]
},
{
name: 'Premiumize',
id: 'premiumize',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://www.premiumize.me/account',
}
]
},
{
name: 'Debrid Link',
id: 'debridlink',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://debrid-link.com/webapp/apikey',
}
]
},
{
name: 'Torbox',
id: 'torbox',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://torbox.app/settings',
}
]
},
{
name: 'Offcloud',
id: 'offcloud',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://offcloud.com/#/account',
}
]
},
{
name: 'put.io',
id: 'putio',
credentials: [
{
label: 'Client ID',
id: 'clientId',
link: 'https://put.io/oauth',
},
{
label: 'Token',
id: 'token',
link: 'https://put.io/oauth',
}
]
},
{
name: 'Easynews',
id: 'easynews',
credentials: [
{
label: 'Username',
id: 'username',
link: 'https://www.easynews.com/',
},
{
label: 'Password',
id: 'password',
link: 'https://www.easynews.com/',
}
]
},
]
export function validateConfig(config: Config): { valid: boolean; errorCode: string | null; errorMessage: string | null } {
const createResponse = (valid: boolean, errorCode: string | null, errorMessage: string | null) => {
return { valid, errorCode, errorMessage };
}
// check for any duplicate addons where both the ID and options are the same
const duplicateAddons = config.addons.filter(
(addon, index) =>
config.addons.findIndex(
(a) =>
a.id === addon.id &&
JSON.stringify(a.options) === JSON.stringify(addon.options)
) !== index
);
if (duplicateAddons.length > 0) {
return createResponse(false, 'duplicateAddons', 'Duplicate addons found. Please remove any duplicates');
}
for (const addon of config.addons) {
// if torbox addon is enabled, torbox service must be enabled and torbox api key must be set
if (addon.id === 'torbox') {
const torboxService = config.services.find(
(service) => service.id === 'torbox'
);
if (!torboxService) {
return createResponse(false, 'torboxServiceNotEnabled', 'Torbox service must be enabled to use the Torbox addon');
}
if (!torboxService.credentials.apiKey) {
return createResponse(false, 'torboxApiKeyNotSet', 'Torbox API Key must be set to use the Torbox addon');
}
}
const details = addonDetails.find((detail) => detail.id === addon.id);
if (!details) {
return createResponse(false, 'invalidAddon', `Invalid addon: ${addon.id}`);
}
if (details.options) {
for (const option of details.options) {
if (option.required && !addon.options[option.id]) {
return createResponse(false, 'missingRequiredOption', `Option ${option.label} is required for addon ${addon.id}`);
}
if (option.id.toLowerCase().includes('url') && addon.options[option.id]) {
console.log('checking url', addon.options[option.id]);
try {
new URL(addon.options[option.id]);
} catch (_) {
return createResponse(false, 'invalidUrl', `Invalid URL for ${option.label}`);
}
}
}
}
}
if (!allowedFormatters.includes(config.formatter)) {
return createResponse(false, 'invalidFormatter', `Invalid formatter: ${config.formatter}`);
}
for (const service of config.services) {
if (service.enabled) {
const serviceDetail = serviceCredentials.find(
(detail) => detail.id === service.id
);
if (!serviceDetail) {
return createResponse(false, 'invalidService', `Invalid service: ${service.id}`);
}
for (const credential of serviceDetail.credentials) {
if (!service.credentials[credential.id]) {
return createResponse(false, 'missingCredential', `${credential.label} is required for ${service.name}`);
}
}
}
}
// need at least one visual tag, resolution, quality
if (config.visualTags.length === 0) {
return createResponse(false, 'noVisualTags', 'At least one visual tag must be selected');
}
if (config.resolutions.length === 0) {
return createResponse(false, 'noResolutions', 'At least one resolution must be selected');
}
if (config.qualities.length === 0) {
return createResponse(false, 'noQualities', 'At least one quality must be selected');
}
if (config.minSize && config.maxSize) {
if (config.minSize >= config.maxSize) {
return createResponse(false, 'invalidSizeRange', 'Your minimum size limit can\'t be greater than or equal to your maximum size limit');
}
}
if (config.addons.length < 1) {
return createResponse(false, 'noAddons', 'At least one addon must be selected');
}
if (config.addons.length > 10) {
return createResponse(false, 'tooManyAddons', 'You can only select a maximum of 10 addons');
}
return createResponse(true, null, null);
};
+1
View File
@@ -1 +1,2 @@
export * from './addon';
export * from './config';
+7 -9
View File
@@ -2,6 +2,7 @@ import express, { Request, Response } from 'express';
import path from 'path';
import { AIOStreams } from './addon';
import { Config, StreamRequest } from '@aiostreams/types';
import { validateConfig } from './config';
import { version, description } from '../package.json';
@@ -135,24 +136,21 @@ app.get('/:config/stream/:type/:id', (req: Request, res: Response) => {
return;
}
try {
const aioStreams = new AIOStreams(configJson);
try {
aioStreams.configValidator();
} catch (error: any) {
console.log(`Invalid config: ${error.message}`);
const { valid, errorCode, errorMessage } = validateConfig(configJson);
if (!valid) {
console.error(`Invalid config: ${errorCode} - ${errorMessage}`);
res.status(200).json({
streams: [
{
url: 'https://example.com',
name: 'Invalid Config',
description: error.message,
description: errorMessage,
},
],
]
})
return;
}
const aioStreams = new AIOStreams(configJson);
aioStreams.getStreams({ id, type, season, episode }).then((streams) => {
res.status(200).json({ streams: streams });
});
+9 -348
View File
@@ -14,6 +14,7 @@ import ServiceInput from '../../components/ServiceInput';
import AddonsList from '../../components/AddonsList';
import { Slide, ToastContainer, toast } from 'react-toastify';
import addonPackage from '../../../package.json';
import { allowedFormatters, allowedLanguages, addonDetails, validateConfig, serviceCredentials } from '@aiostreams/config';
const version = addonPackage.version;
@@ -51,86 +52,6 @@ const defaultSortCriteria: SortBy[] = [
{ seeders: false },
];
const allowedFormatters = ['gdrive', 'torrentio', 'torbox'];
interface AddonDetail {
name: string;
id: string;
options?: {
id: string;
required?: boolean;
label: string;
description?: string;
type: 'text' | 'checkbox';
}[];
}
const addonDetails: AddonDetail[] = [
{
name: 'Torrentio',
id: 'torrentio',
options: [
{
id: 'overrideUrl',
required: false,
label: 'Override URL',
description:
'Override the URL used to fetch streams from the torrentio addon',
type: 'text',
},
{
id: 'useMultipleInstances',
required: false,
label: 'Use Multiple Instances',
description:
'Use multiple instances of the torrentio addon to fetch streams when using multiple services',
type: 'checkbox',
},
],
},
{
name: 'Torbox',
id: 'torbox',
},
{
name: 'Google Drive (Viren070)',
id: 'gdrive',
options: [
{
id: 'addonUrl',
required: true,
label: 'Addon URL',
description: 'The URL of the Google Drive addon',
type: 'text',
},
],
},
{
name: 'Custom',
id: 'custom',
options: [
{
id: 'url',
required: true,
description: 'The URL of the custom addon',
label: 'URL',
type: 'text',
},
{
id: 'name',
required: true,
description: 'The name of the custom addon',
label: 'Name',
type: 'text',
}
],
},
];
const allowedLanguages = ['English', 'Spanish', 'French', 'German', 'Chinese'];
function showToast(
message: string,
type: 'success' | 'error' | 'info' | 'warning',
@@ -219,106 +140,7 @@ const defaultServices = [
];
const serviceCredentials = [
{
name: 'Real Debrid',
id: 'realdebrid',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://real-debrid.com/apitoken',
}
]
},
{
name: 'All Debrid',
id: 'alldebrid',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://alldebrid.com/api',
}
]
},
{
name: 'Premiumize',
id: 'premiumize',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://www.premiumize.me/account',
}
]
},
{
name: 'Debrid Link',
id: 'debridlink',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://debrid-link.com/webapp/apikey',
}
]
},
{
name: 'Torbox',
id: 'torbox',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://torbox.app/settings',
}
]
},
{
name: 'Offcloud',
id: 'offcloud',
credentials: [
{
label: 'API Key',
id: 'apiKey',
link: 'https://offcloud.com/#/account',
}
]
},
{
name: 'put.io',
id: 'putio',
credentials: [
{
label: 'Client ID',
id: 'clientId',
link: 'https://put.io/oauth',
},
{
label: 'Token',
id: 'token',
link: 'https://put.io/oauth',
}
]
},
{
name: 'Easynews',
id: 'easynews',
credentials: [
{
label: 'Username',
id: 'username',
link: 'https://www.easynews.com/',
},
{
label: 'Password',
id: 'password',
link: 'https://www.easynews.com/',
}
]
},
]
export default function Configure() {
const [resolutions, setResolutions] =
@@ -382,180 +204,19 @@ export default function Configure() {
return `${protocol}//${root}/${encodedConfig}/manifest.json`;
};
const validateConfig = () => {
const createAndValidateConfig = () => {
const config = createConfig();
// check for any duplicate addons where both the ID and options are the same
const duplicateAddons = config.addons.filter(
(addon, index) =>
config.addons.findIndex(
(a) =>
a.id === addon.id &&
JSON.stringify(a.options) === JSON.stringify(addon.options)
) !== index
);
if (duplicateAddons.length > 0) {
showToast(
'Duplicate addons found. Please remove any duplicates',
'error',
'duplicateAddons'
);
return false
}
for (const addon of config.addons) {
// if torbox addon is enabled, torbox service must be enabled and torbox api key must be set
if (addon.id === 'torbox') {
const torboxService = config.services.find(
(service) => service.id === 'torbox'
);
if (!torboxService) {
showToast(
'Torbox service must be enabled to use the Torbox addon',
'error',
'torboxServiceNotEnabled'
);
return false;
}
if (!torboxService.credentials.apiKey) {
showToast(
'Torbox API Key must be set to use the Torbox addon',
'error',
'torboxApiKeyNotSet'
);
return false;
}
}
const details = addonDetails.find((detail) => detail.id === addon.id);
if (!details) {
showToast(`Invalid addon: ${addon.id}`, 'error', 'invalidAddon');
return false;
}
if (details.options) {
for (const option of details.options) {
if (option.required && !addon.options[option.id]) {
showToast(
`Option ${option.label} is required for addon ${addon.id}`,
'error',
'missingRequiredOption'
);
return false;
}
console.log(option.label);
if (option.id.toLowerCase().includes('url')) {
try {
new URL(addon.options[option.id]);
} catch (_) {
showToast(
`Invalid URL for ${option.label}`,
'error',
'invalidUrl'
);
return false;
}
}
}
}
}
if (!allowedFormatters.includes(config.formatter)) {
showToast(
`Invalid formatter: ${config.formatter}`,
'error',
'invalidFormatter'
);
const { valid, errorCode, errorMessage } = validateConfig(config);
if (!valid) {
showToast(errorMessage || 'Invalid config', 'error', errorCode || 'error');
return false;
}
for (const service of config.services) {
if (service.enabled) {
const serviceDetail = serviceCredentials.find(
(detail) => detail.id === service.id
);
if (!serviceDetail) {
showToast(`Invalid service: ${service.id}`, 'error', 'invalidService');
return false;
}
for (const credential of serviceDetail.credentials) {
if (!service.credentials[credential.id]) {
showToast(
`${credential.label} is required for ${service.name}`,
'error',
`missing${service.id}${credential.id}`
);
return false;
}
}
}
}
// need at least one visual tag, resolution, quality
if (config.visualTags.length === 0) {
showToast(
'At least one visual tag must be selected',
'error',
'noVisualTags'
);
return false;
}
if (config.resolutions.length === 0) {
showToast(
'At least one resolution must be selected',
'error',
'noResolutions'
);
return false;
}
if (config.qualities.length === 0) {
showToast(
'At least one quality must be selected',
'error',
'noQualities'
);
return false;
}
if (config.minSize && config.maxSize) {
if (config.minSize > config.maxSize) {
showToast(
"Your minimum size limit can't be greater than your maximum size limit",
'error',
'invalidSizeRange'
);
return false;
} else if (config.minSize === config.maxSize) {
setTimeout(() => {
showToast(
'Your minimum and maximum size are the same, this will result in no streams being shown',
'warning',
'sameSize'
);
}, 500);
}
}
if (config.addons.length < 1) {
showToast('At least one addon must be selected', 'error', 'noAddons');
return false;
}
if (config.addons.length > 10) {
showToast(
'You can only select a maximum of 10 addons',
'error',
'tooManyAddons'
);
}
return true;
};
const handleInstall = () => {
if (validateConfig()) {
if (createAndValidateConfig()) {
const manifestUrl = getManifestUrl();
const stremioUrl = manifestUrl.replace(/^https?/, 'stremio');
showToast(
@@ -571,7 +232,7 @@ export default function Configure() {
};
const handleInstallToWeb = () => {
if (validateConfig()) {
if (createAndValidateConfig()) {
const manifestUrl = getManifestUrl();
const encodedManifestUrl = encodeURIComponent(manifestUrl);
showToast(
@@ -590,7 +251,7 @@ export default function Configure() {
};
const handleCopyLink = () => {
if (validateConfig()) {
if (createAndValidateConfig()) {
const manifestUrl = getManifestUrl();
navigator.clipboard.writeText(manifestUrl).then(() => {
showToast(
+27 -10
View File
@@ -1,17 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"composite": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
@@ -20,14 +20,31 @@
}
],
"paths": {
"@/*": ["./src/*"]
}
"@/*": [
"./src/*"
],
"@aiostreams/config": [
"../addon/src/config"
]
},
"noEmit": false,
"isolatedModules": true
},
"references": [
{
"path": "../types"
},
{
"path": "../addon"
}
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+13
View File
@@ -113,3 +113,16 @@ export interface Config {
credentials: { [key: string]: string };
}[];
}
export interface AddonDetail {
name: string;
id: string;
options?: {
id: string;
required?: boolean;
label: string;
description?: string;
type: 'text' | 'checkbox';
}[];
}