diff --git a/.env.sample b/.env.sample index 2f01384e..210b5868 100644 --- a/.env.sample +++ b/.env.sample @@ -644,6 +644,10 @@ PRUNE_MAX_DAYS=-1 # FORCE_STREMTHRU_TORZ_PORT= # FORCE_STREMTHRU_TORZ_PROTOCOL= +# --------- Sootio --------- +# SOOTIO_URL=https://sootio.elfhosted.com +# DEFAULT_SOOTIO_TIMEOUT= + # --------- EASYNEWS+ ADDON --------- # EASYNEWS_PLUS_URL=https://b89262c192b0-stremio-easynews-addon.baby-beamup.club/ # DEFAULT_EASYNEWS_PLUS_TIMEOUT= diff --git a/packages/core/src/presets/presetManager.ts b/packages/core/src/presets/presetManager.ts index ced88007..9806101e 100644 --- a/packages/core/src/presets/presetManager.ts +++ b/packages/core/src/presets/presetManager.ts @@ -63,6 +63,7 @@ import { JackettPreset } from './jackett.js'; import { NZBHydraPreset } from './nzbhydra.js'; import { KnabenPreset } from './knaben.js'; import { BitmagnetPreset } from './bitmagnet.js'; +import { SootioPreset } from './sootio.js'; let PRESET_LIST: string[] = [ 'custom', @@ -74,6 +75,7 @@ let PRESET_LIST: string[] = [ 'mediafusion', 'stremthruTorz', 'stremthruStore', + 'sootio', 'zilean', 'knaben', Env.BUILTIN_BITMAGNET_URL ? 'bitmagnet' : '', @@ -272,6 +274,8 @@ export class PresetManager { return KnabenPreset; case 'bitmagnet': return BitmagnetPreset; + case 'sootio': + return SootioPreset; default: throw new Error(`Preset ${id} not found`); } diff --git a/packages/core/src/presets/sootio.ts b/packages/core/src/presets/sootio.ts new file mode 100644 index 00000000..99377509 --- /dev/null +++ b/packages/core/src/presets/sootio.ts @@ -0,0 +1,175 @@ +import { + Addon, + Option, + UserData, + Resource, + Stream, + ParsedStream, +} from '../db/index.js'; +import { baseOptions, Preset } from './preset.js'; +import { createLogger, Env, getSimpleTextHash } from '../utils/index.js'; +import { constants, ServiceId } from '../utils/index.js'; +import { StreamParser } from '../parser/index.js'; + +class SootioStreamParser extends StreamParser { + protected override getInLibrary( + stream: Stream, + currentParsedStream: ParsedStream + ): boolean { + return stream.description?.includes('[Cloud]') ?? false; + } + protected override getIndexer( + stream: Stream, + currentParsedStream: ParsedStream + ): string | undefined { + return /[☁️💾][^|]*\|\s*(.*)$/s.exec(stream.description || '')?.[1]; + } +} + +export class SootioPreset extends Preset { + static override getParser(): typeof StreamParser { + return SootioStreamParser; + } + + static override get METADATA() { + const supportedServices: ServiceId[] = [ + constants.REALDEBRID_SERVICE, + constants.OFFCLOUD_SERVICE, + ]; + + const supportedResources = [ + constants.STREAM_RESOURCE, + constants.CATALOG_RESOURCE, + ]; + + const options: Option[] = [ + ...baseOptions( + 'Sootio', + supportedResources, + Env.DEFAULT_SOOTIO_TIMEOUT, + Env.SOOTIO_URL + ), + { + id: 'services', + name: 'Services', + description: + 'Optionally override the services that are used. If not specified, then the services that are enabled and supported will be used.', + type: 'multi-select', + required: false, + showInNoobMode: false, + options: supportedServices.map((service) => ({ + value: service, + label: constants.SERVICE_DETAILS[service].name, + })), + default: undefined, + emptyIsUndefined: true, + }, + { + id: 'socials', + name: '', + description: '', + type: 'socials', + socials: [ + { + id: 'github', + url: 'https://github.com/sooti/stremio-addon-debrid-search', + }, + ], + }, + ]; + + return { + ID: 'sootio', + NAME: 'Sootio', + LOGO: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2364ffda;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%2300A7B5;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23grad)' d='M50,5 C74.85,5 95,25.15 95,50 C95,74.85 74.85,95 50,95 C35,95 22.33,87.6 15,76 C25,85 40,85 50,80 C60,75 65,65 65,50 C65,35 55,25 40,25 C25,25 15,40 15,50 C15,55 16,60 18,64 C8.5,58 5,45 5,50 C5,25.15 25.15,5 50,5 Z'/%3E%3C/svg%3E`, + URL: Env.SOOTIO_URL[0], + TIMEOUT: Env.DEFAULT_SOOTIO_TIMEOUT || Env.DEFAULT_TIMEOUT, + USER_AGENT: Env.DEFAULT_SOOTIO_USER_AGENT || Env.DEFAULT_USER_AGENT, + SUPPORTED_SERVICES: supportedServices, + DESCRIPTION: 'Debrid addon.', + OPTIONS: options, + SUPPORTED_STREAM_TYPES: [constants.DEBRID_STREAM_TYPE], + SUPPORTED_RESOURCES: supportedResources, + }; + } + + static async generateAddons( + userData: UserData, + options: Record + ): Promise { + if (options?.url?.endsWith('/manifest.json')) { + return [this.generateAddon(userData, options, undefined)]; + } + + const usableServices = this.getUsableServices(userData, options.services); + + if (!usableServices || usableServices.length === 0) { + throw new Error( + `${this.METADATA.NAME} requires at least one usable service, but none were found. Please enable at least one of the following services: ${this.METADATA.SUPPORTED_SERVICES.join( + ', ' + )}` + ); + } + + let addons = usableServices.map((service) => { + return this.generateAddon(userData, options, service.id); + }); + + return addons; + } + + private static generateAddon( + userData: UserData, + options: Record, + serviceId?: ServiceId + ): Addon { + return { + name: options.name || this.METADATA.NAME, + identifier: serviceId + ? `${constants.SERVICE_DETAILS[serviceId].shortName}` + : undefined, + displayIdentifier: serviceId + ? `${constants.SERVICE_DETAILS[serviceId].shortName}` + : undefined, + manifestUrl: this.generateManifestUrl(userData, options, serviceId), + enabled: true, + resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, + timeout: options.timeout || this.METADATA.TIMEOUT, + preset: { + id: '', + type: this.METADATA.ID, + options: options, + }, + headers: { + 'User-Agent': this.METADATA.USER_AGENT, + }, + }; + } + + private static generateManifestUrl( + userData: UserData, + options: Record, + serviceId?: ServiceId + ) { + const url = (options.url || this.METADATA.URL).replace(/\/$/, ''); + if (url.endsWith('/manifest.json')) { + return url; + } + if (!serviceId) { + throw new Error( + `${this.METADATA.NAME} requires at least one usable service, but none were found. Please enable at least one of the following services: ${this.METADATA.SUPPORTED_SERVICES.join( + ', ' + )}` + ); + } + const serviceNameMap: Partial> = { + realdebrid: 'RealDebrid', + offcloud: 'OffCloud', + }; + const config = { + DebridProvider: serviceNameMap[serviceId], + DebridApiKey: this.getServiceCredential(serviceId, userData), + }; + return `${url}/${this.urlEncodeJSON(config)}/manifest.json`; + } +} diff --git a/packages/core/src/utils/env.ts b/packages/core/src/utils/env.ts index 12227308..2f7f0e91 100644 --- a/packages/core/src/utils/env.ts +++ b/packages/core/src/utils/env.ts @@ -1185,6 +1185,19 @@ export const Env = cleanEnv(process.env, { desc: 'Default StreamFusion StremThru URL', }), + SOOTIO_URL: urlOrUrlList({ + default: ['https://sootio.elfhosted.com'], + desc: 'Sootio URL', + }), + DEFAULT_SOOTIO_TIMEOUT: num({ + default: undefined, + desc: 'Default Sootio timeout', + }), + DEFAULT_SOOTIO_USER_AGENT: userAgent({ + default: undefined, + desc: 'Default Sootio user agent', + }), + // DMM Cast settings DEFAULT_DMM_CAST_TIMEOUT: num({ default: undefined,