diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 641a50fd..0e268aff 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -306,7 +306,7 @@ export class AIOStreams { switch (addonId) { case 'torbox': { - return await getTorboxStreams(this.config, streamRequest); + return await getTorboxStreams(this.config, addon.options, streamRequest); } case 'torrentio': { return await getTorrentioStreams( @@ -315,15 +315,17 @@ export class AIOStreams { streamRequest ); } - case 'gdrive': - const addonUrl = addon.options.addonUrl.replace('/manifest.json', '/'); - const wrapper = new BaseWrapper('GDrive', addonUrl); + case 'gdrive': { + let addonUrl = addon.options.addonUrl as string; + addonUrl = addonUrl.replace('/manifest.json', '/') + const wrapper = new BaseWrapper('GDrive', addonUrl, addon.options.indexerTimeout ? addon.options.indexerTimeout as number : undefined); return await wrapper.getParsedStreams(streamRequest); + } default: { console.log( `Using base wrapper for addon ${addon.options.name} with url ${addon.options.url}` ); - const wrapper = new BaseWrapper(addon.options.name, addon.options.url); + const wrapper = new BaseWrapper(addon.options.name as string, addon.options.url as string, addon.options.indexerTimeout ? addon.options.indexerTimeout as number : undefined); return await wrapper.getParsedStreams(streamRequest); } } diff --git a/packages/addon/src/config.ts b/packages/addon/src/config.ts index 8b04dd09..d5726dd1 100644 --- a/packages/addon/src/config.ts +++ b/packages/addon/src/config.ts @@ -14,7 +14,6 @@ export const addonDetails: AddonDetail[] = [ { id: 'overrideUrl', required: false, - label: 'Override URL', description: 'Override the URL used to fetch streams from the torrentio addon', @@ -23,17 +22,40 @@ export const addonDetails: AddonDetail[] = [ { 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', }, + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: 'The timeout for fetching streams from the Torrentio addon', + type: 'number', + constraints: { + min: 1000, + max: 20000 + } + } ], }, { name: 'Torbox', id: 'torbox', + options: [ + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: 'The timeout for fetching streams from the Torbox addon in milliseconds', + type: 'number', + constraints: { + min: 1000, + max: 20000 + } + } + ], }, { name: 'Google Drive (Viren070)', @@ -46,6 +68,17 @@ export const addonDetails: AddonDetail[] = [ description: 'The URL of the Google Drive addon', type: 'text', }, + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: 'The timeout for fetching streams from the Google Drive addon in milliseconds', + type: 'number', + constraints: { + min: 1000, + max: 20000 + } + } ], }, { @@ -66,6 +99,17 @@ export const addonDetails: AddonDetail[] = [ label: 'Name', type: 'text', }, + { + id: 'indexerTimeout', + required: false, + label: 'Override Indexer Timeout', + description: 'The timeout for fetching streams from the custom addon in milliseconds', + type: 'number', + constraints: { + min: 1000, + max: 20000 + } + } ], }, ]; @@ -255,7 +299,7 @@ export function validateConfig(config: Config): { ) { console.log('checking url', addon.options[option.id]); try { - new URL(addon.options[option.id]); + new URL(addon.options[option.id] as string); } catch (_) { return createResponse( false, @@ -264,6 +308,39 @@ export function validateConfig(config: Config): { ); } } + + if (option.type === 'number' && addon.options[option.id]) { + if (typeof addon.options[option.id] !== 'number') { + return createResponse( + false, + 'invalidNumber', + `${option.label} must be a number` + ); + } + + if (option.constraints) { + if ( + option.constraints.min !== undefined && + addon.options[option.id] as number < option.constraints.min + ) { + return createResponse( + false, + 'invalidNumber', + `${option.label} must be greater than or equal to ${option.constraints.min}` + ); + } + if ( + option.constraints.max !== undefined && + addon.options[option.id] as number > option.constraints.max + ) { + return createResponse( + false, + 'invalidNumber', + `${option.label} must be less than or equal to ${option.constraints.max}` + ); + } + } + } } } } diff --git a/packages/frontend/src/components/AddonsList.module.css b/packages/frontend/src/components/AddonsList.module.css index 77087195..37ed17fe 100644 --- a/packages/frontend/src/components/AddonsList.module.css +++ b/packages/frontend/src/components/AddonsList.module.css @@ -90,7 +90,9 @@ align-items: center; } -.option input[type='text'] { +.option input[type='text'], +.option input[type='number'] +{ padding: 5px; border-radius: 4px; border: 1px solid #555; diff --git a/packages/frontend/src/components/AddonsList.tsx b/packages/frontend/src/components/AddonsList.tsx index 89e5952d..e249f63d 100644 --- a/packages/frontend/src/components/AddonsList.tsx +++ b/packages/frontend/src/components/AddonsList.tsx @@ -1,28 +1,12 @@ import React, { useState } from 'react'; import styles from './AddonsList.module.css'; - -interface AddonDetail { - name: string; - id: string; - options?: { - id: string; - required?: boolean; - label: string; - description?: string; - type: 'text' | 'checkbox'; - }[]; -} - -interface Addon { - id: string; - options: { [key: string]: string }; -} +import { AddonDetail, Config } from '@aiostreams/types'; interface AddonsListProps { choosableAddons: string[]; addonDetails: AddonDetail[]; - addons: Addon[]; - setAddons: (addons: Addon[]) => void; + addons: Config['addons']; + setAddons: (addons: Config['addons']) => void; } const AddonsList: React.FC = ({ @@ -49,10 +33,10 @@ const AddonsList: React.FC = ({ const updateOption = ( addonIndex: number, optionKey: string, - value: string + value?: string | boolean | number ) => { const newAddons = [...addons]; - newAddons[addonIndex].options[optionKey] = value.trim(); + newAddons[addonIndex].options[optionKey] = typeof value === 'string' ? value.trim() : value; setAddons(newAddons); }; @@ -108,12 +92,12 @@ const AddonsList: React.FC = ({ {option.type === 'checkbox' && ( updateOption( index, option.id, - e.target.checked.toString() + e.target.checked ) } className={styles.checkbox} @@ -124,9 +108,19 @@ const AddonsList: React.FC = ({ {option.type === 'text' && ( - updateOption(index, option.id, e.target.value) + updateOption(index, option.id, e.target.value ? e.target.value : undefined) + } + className={styles.textInput} + /> + )} + {option.type === 'number' && ( + + updateOption(index, option.id, e.target.value ? parseInt(e.target.value) : undefined) } className={styles.textInput} /> diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 2e9a56cd..711a3f59 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -108,7 +108,7 @@ export interface Config { minSize: number | null; addons: { id: string; - options: { [key: string]: string }; + options: { [key: string]: string | boolean | number | undefined }; }[]; services: { name: string; @@ -118,14 +118,34 @@ export interface Config { }[]; } + +interface BaseOptionDetail { + id: string; + required?: boolean; + label: string; + description?: string; +} + +export interface TextOptionDetail extends BaseOptionDetail { + type: 'text'; +} + +export interface CheckboxOptionDetail extends BaseOptionDetail { + type: 'checkbox'; +} + +export interface NumberOptionDetail extends BaseOptionDetail { + type: 'number'; + constraints: { + min?: number; + max?: number; + } +} + +export type AddonOptionDetail = TextOptionDetail | CheckboxOptionDetail | NumberOptionDetail; + export interface AddonDetail { name: string; id: string; - options?: { - id: string; - required?: boolean; - label: string; - description?: string; - type: 'text' | 'checkbox'; - }[]; + options?: AddonOptionDetail[]; } diff --git a/packages/wrappers/src/torbox.ts b/packages/wrappers/src/torbox.ts index 9c1988e2..59cb04f1 100644 --- a/packages/wrappers/src/torbox.ts +++ b/packages/wrappers/src/torbox.ts @@ -19,8 +19,8 @@ interface TorboxStream { export class Torbox extends BaseWrapper { private readonly name: string = 'Torbox'; - constructor(apiKey: string) { - super('Torbox', 'https://stremio.torbox.app/' + apiKey + '/', 10000); + constructor(apiKey: string, indexerTimeout: number = 10000) { + super('Torbox', 'https://stremio.torbox.app/' + apiKey + '/', indexerTimeout); } protected parseStream(stream: TorboxStream): ParsedStream | undefined { @@ -100,6 +100,9 @@ export class Torbox extends BaseWrapper { export async function getTorboxStreams( config: Config, + torboxOptions: { + indexerTimeout?: number; + }, streamRequest: StreamRequest ): Promise { const torboxService = config.services.find( @@ -114,6 +117,6 @@ export async function getTorboxStreams( throw new Error('Torbox API key not found'); } - const torbox = new Torbox(torboxApiKey); + const torbox = new Torbox(torboxApiKey, torboxOptions.indexerTimeout); return await torbox.getParsedStreams(streamRequest); } diff --git a/packages/wrappers/src/torrentio.ts b/packages/wrappers/src/torrentio.ts index 8739bdd9..c93a87aa 100644 --- a/packages/wrappers/src/torrentio.ts +++ b/packages/wrappers/src/torrentio.ts @@ -6,7 +6,7 @@ import { BaseWrapper } from './base'; export class Torrentio extends BaseWrapper { private readonly name: string = 'Torrentio'; - constructor(configString: string | null, overrideUrl: string | null) { + constructor(configString: string | null, overrideUrl: string | null, indexerTimeout: number = 10000) { if (overrideUrl && overrideUrl.endsWith('/manifest.json')) { overrideUrl = overrideUrl.replace('/manifest.json', '/'); } @@ -16,7 +16,7 @@ export class Torrentio extends BaseWrapper { : 'https://torrentio.strem.fun/' + (configString ? configString + '/' : ''); - super('Torrentio', url); + super('Torrentio', url, indexerTimeout); } protected parseStream(stream: Stream): ParsedStream { @@ -68,7 +68,11 @@ export class Torrentio extends BaseWrapper { export async function getTorrentioStreams( config: Config, - torrentioOptions: { [key: string]: string }, + torrentioOptions: { + useMultipleInstances?: boolean; + overrideUrl?: string; + indexerTimeout?: number; + }, streamRequest: StreamRequest ): Promise { const supportedServices: string[] = [ @@ -84,7 +88,7 @@ export async function getTorrentioStreams( // If overrideUrl is provided, use it to get streams and skip all other steps if (torrentioOptions.overrideUrl) { - const torrentio = new Torrentio(null, torrentioOptions.overrideUrl); + const torrentio = new Torrentio(null, torrentioOptions.overrideUrl as string, torrentioOptions.indexerTimeout); return torrentio.getParsedStreams(streamRequest); } @@ -95,7 +99,7 @@ export async function getTorrentioStreams( // if no usable services found, use torrentio without any configuration if (usableServices.length < 0) { - const torrentio = new Torrentio(null, null); + const torrentio = new Torrentio(null, null, torrentioOptions.indexerTimeout); return await torrentio.getParsedStreams(streamRequest); } @@ -117,7 +121,7 @@ export async function getTorrentioStreams( } console.log('Creating Torrentio instance with service:', service.id); let configString = getServicePair(service.id, service.credentials); - const torrentio = new Torrentio(configString, null); + const torrentio = new Torrentio(configString, null, torrentioOptions.indexerTimeout); const streams = await torrentio.getParsedStreams(streamRequest); parsedStreams.push(...streams); } @@ -130,7 +134,7 @@ export async function getTorrentioStreams( } configString += getServicePair(service.id, service.credentials) + '|'; } - const torrentio = new Torrentio(configString, null); + const torrentio = new Torrentio(configString, null, torrentioOptions.indexerTimeout); return await torrentio.getParsedStreams(streamRequest); } }