From 0c9c86c218c5754e62ff94c0d26d398f32da92a1 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Sat, 14 Jun 2025 19:13:47 +0100 Subject: [PATCH] feat!: stuff --- packages/core/src/db/schemas.ts | 31 +- packages/core/src/main.ts | 201 +++--- packages/core/src/presets/aiostreams.ts | 3 +- packages/core/src/presets/animeKitsu.ts | 3 +- packages/core/src/presets/comet.ts | 3 +- packages/core/src/presets/custom.ts | 3 +- packages/core/src/presets/dcUniverse.ts | 3 +- packages/core/src/presets/debridio.ts | 3 +- packages/core/src/presets/debridioTmdb.ts | 3 +- packages/core/src/presets/debridioTv.ts | 3 +- packages/core/src/presets/debridioTvdb.ts | 3 +- .../core/src/presets/debridioWatchtower.ts | 6 +- packages/core/src/presets/dmmCast.ts | 3 +- packages/core/src/presets/easynews.ts | 3 +- packages/core/src/presets/jackettio.ts | 3 +- packages/core/src/presets/marvel.ts | 3 +- packages/core/src/presets/mediafusion.ts | 4 +- packages/core/src/presets/nuviostreams.ts | 3 +- packages/core/src/presets/opensubtitles.ts | 3 +- packages/core/src/presets/orion.ts | 3 +- packages/core/src/presets/peerflix.ts | 3 +- packages/core/src/presets/presetManager.ts | 18 +- packages/core/src/presets/rpdbCatalogs.ts | 3 +- packages/core/src/presets/starWarsUniverse.ts | 3 +- packages/core/src/presets/streamfusion.ts | 3 +- packages/core/src/presets/stremthruStore.ts | 3 +- packages/core/src/presets/stremthruTorz.ts | 3 +- packages/core/src/presets/tmdbCollections.ts | 3 +- packages/core/src/presets/torbox.ts | 3 +- packages/core/src/presets/torrentCatalogs.ts | 3 +- packages/core/src/presets/torrentio.ts | 3 +- packages/core/src/utils/config.ts | 16 +- packages/core/src/utils/crypto.ts | 39 +- packages/core/src/utils/regex.ts | 10 +- packages/core/src/wrapper.ts | 4 +- .../frontend/src/components/menu/addons.tsx | 571 +++++++++++------- .../frontend/src/components/menu/filters.tsx | 28 +- .../src/components/menu/formatter.tsx | 2 + .../frontend/src/components/menu/proxy.tsx | 4 +- packages/server/src/routes/api/catalog.ts | 2 +- 40 files changed, 661 insertions(+), 353 deletions(-) diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index 31440af5..81b3e9cd 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -105,7 +105,9 @@ export type Resource = z.infer; const ResourceList = z.array(ResourceSchema); const AddonSchema = z.object({ - id: z.string().min(1).optional(), + instanceId: z.string().min(1).optional(), // uniquely identifies the addon in a given list of addons + presetType: z.string().min(1), // reference to the type of the preset that created this addon + presetInstanceId: z.string().min(1), // reference to the instance id of the preset that created this addon manifestUrl: z.string().url(), enabled: z.boolean(), resources: ResourceList.optional(), @@ -114,21 +116,20 @@ const AddonSchema = z.object({ timeout: z.number().min(1), library: z.boolean().optional(), streamPassthrough: z.boolean().optional(), - fromPresetId: z.string().min(1).optional(), headers: z.record(z.string().min(1), z.string().min(1)).optional(), ip: z.string().ip().optional(), }); // preset objects are transformed into addons by a preset transformer. const PresetSchema = z.object({ - id: z.string().min(1), + type: z.string().min(1), // the preset type e.g. 'torrentio' + instanceId: z.string().min(1), // uniquely identifies the preset in a given list of presets enabled: z.boolean(), options: z.record(z.string().min(1), z.any()), }); export type PresetObject = z.infer; -const AddonList = z.array(AddonSchema); const PresetList = z.array(PresetSchema); export type Addon = z.infer; @@ -500,6 +501,7 @@ const MetaLinkSchema = z.object({ const MetaVideoSchema = z.object({ id: z.string().min(1), title: z.string().optional(), + name: z.string().optional(), released: z.string().datetime().optional(), thumbnail: z.string().url().or(z.null()).optional(), streams: z.array(StreamSchema).optional(), @@ -722,6 +724,24 @@ const PresetMetadataSchema = z.object({ SUPPORTED_RESOURCES: z.array(ResourceSchema), }); +const PresetMinimalMetadataSchema = z.object({ + ID: z.string(), + NAME: z.string(), + LOGO: z.string(), + DESCRIPTION: z.string(), + URL: z.string(), + DISABLED: z + .object({ + reason: z.string(), + disabled: z.boolean(), + }) + .optional(), + SUPPORTED_RESOURCES: z.array(ResourceSchema), + SUPPORTED_STREAM_TYPES: z.array(StreamTypes), + SUPPORTED_SERVICES: z.array(z.string()), + OPTIONS: z.array(OptionDefinition), +}); + const StatusResponseSchema = z.object({ version: z.string(), tag: z.string(), @@ -758,7 +778,7 @@ const StatusResponseSchema = z.object({ }), timeout: z.number().or(z.null()), }), - presets: z.array(PresetMetadataSchema), + presets: z.array(PresetMinimalMetadataSchema), services: z.record( z.enum(constants.SERVICES), z.object({ @@ -775,3 +795,4 @@ const StatusResponseSchema = z.object({ export type StatusResponse = z.infer; export type PresetMetadata = z.infer; +export type PresetMinimalMetadata = z.infer; diff --git a/packages/core/src/main.ts b/packages/core/src/main.ts index 6aa131cb..ff22d0bb 100644 --- a/packages/core/src/main.ts +++ b/packages/core/src/main.ts @@ -38,6 +38,7 @@ import { createFormatter } from './formatters'; import { compileRegex, formRegexFromKeywords, + parseRegex, safeRegexTest, } from './utils/regex'; import { isMatch } from 'super-regex'; @@ -59,8 +60,8 @@ export interface AIOStreamsResponse { export class AIOStreams { private readonly userData: UserData; - private manifests: Record; - private supportedResources: Record; + private manifests: Record; + private supportedResources: Record; private finalResources: StrictManifestResource[] = []; private finalCatalogs: Manifest['catalogs'] = []; private finalAddonCatalogs: Manifest['addonCatalogs'] = []; @@ -244,16 +245,16 @@ export class AIOStreams { // get the addon index from the id logger.info(`Handling catalog request`, { type, id, extras }); const start = Date.now(); - const addonIndex = id.split('.', 2)[0]; - const addon = this.getAddon(Number(addonIndex)); + const addonInstanceId = id.split('.', 2)[0]; + const addon = this.getAddon(addonInstanceId); if (!addon) { - logger.error(`Addon ${addonIndex} not found`); + logger.error(`Addon ${addonInstanceId} not found`); return { success: false, data: [], errors: [ { - title: `Addon ${addonIndex} not found`, + title: `Addon ${addonInstanceId} not found`, description: 'Addon not found', }, ], @@ -345,7 +346,9 @@ export class AIOStreams { logger.info(`Handling meta request`, { type, id }); // step 1 // First try to find an addon that has a matching idPrefix - for (const [index, resources] of Object.entries(this.supportedResources)) { + for (const [instanceId, resources] of Object.entries( + this.supportedResources + )) { const resource = resources.find( (r) => r.name === 'meta' && @@ -353,10 +356,13 @@ export class AIOStreams { r.idPrefixes?.some((prefix) => id.startsWith(prefix)) ); if (resource) { - const addon = this.getAddon(Number(index)); + const addon = this.getAddon(instanceId); + if (!addon) { + continue; + } logger.info(`Found addon with matching id prefix for meta resource`, { addonName: addon.name, - addonIndex: index, + addonInstanceId: instanceId, }); try { const meta = await new Wrapper(addon).getMeta(type, id); @@ -387,15 +393,20 @@ export class AIOStreams { // step 2 // If no matching prefix found, use any addon that supports meta for this type - for (const [index, resources] of Object.entries(this.supportedResources)) { + for (const [instanceId, resources] of Object.entries( + this.supportedResources + )) { const resource = resources.find( (r) => r.name === 'meta' && r.types.includes(type) ); if (resource) { - const addon = this.getAddon(Number(index)); + const addon = this.getAddon(instanceId); + if (!addon) { + continue; + } logger.info(`Using fallback addon for meta resource`, { addonName: addon.name, - addonIndex: index, + addonInstanceId: instanceId, }); try { const meta = await new Wrapper(addon).getMeta(type, id); @@ -438,7 +449,7 @@ export class AIOStreams { // Find all addons that support subtitles for this type and id prefix const supportedAddons = []; - for (const [addonIndex, addonResources] of Object.entries( + for (const [instanceId, addonResources] of Object.entries( this.supportedResources )) { const resource = addonResources.find( @@ -450,7 +461,7 @@ export class AIOStreams { : true) ); if (resource) { - const addon = this.getAddon(Number(addonIndex)); + const addon = this.getAddon(instanceId); if (addon) { supportedAddons.push(addon); } @@ -501,16 +512,16 @@ export class AIOStreams { ): Promise> { logger.info(`getAddonCatalog: ${id}`); // step 1 - // get the addon index from the id - const addonIndex = id.split('.', 2)[0]; - const addon = this.getAddon(Number(addonIndex)); + // get the addon instance id from the id + const addonInstanceId = id.split('.', 2)[0]; + const addon = this.getAddon(addonInstanceId); if (!addon) { return { success: false, data: [], errors: [ { - title: `Addon ${addonIndex} not found`, + title: `Addon ${addonInstanceId} not found`, description: 'Addon not found', }, ], @@ -556,15 +567,15 @@ export class AIOStreams { } for (const preset of this.userData.presets.filter((p) => p.enabled)) { - const addons = await PresetManager.fromId(preset.id).generateAddons( + const addons = await PresetManager.fromId(preset.type).generateAddons( this.userData, preset.options ); - this.addons.push( ...addons.map((a) => ({ ...a, - id: JSON.stringify(preset), + presetInstanceId: preset.instanceId, + instanceId: `${preset.instanceId}${getSimpleTextHash(`${a.manifestUrl}`).slice(0, 4)}`, })) ); } @@ -579,10 +590,10 @@ export class AIOStreams { private async fetchManifests() { this.manifests = Object.fromEntries( await Promise.all( - this.addons.map(async (addon, index) => { + this.addons.map(async (addon) => { try { this.validateAddon(addon); - return [index, await new Wrapper(addon).getManifest()]; + return [addon.instanceId, await new Wrapper(addon).getManifest()]; } catch (error: any) { await this.handlePossibleRecursiveError(error); if (this.skipFailedAddons) { @@ -591,7 +602,7 @@ export class AIOStreams { error: error.message, }); logger.error(`${error.message}, skipping`); - return [index, null]; + return [addon.instanceId, null]; } throw error; } @@ -601,7 +612,7 @@ export class AIOStreams { } private async fetchResources() { - for (const [index, manifest] of Object.entries(this.manifests)) { + for (const [instanceId, manifest] of Object.entries(this.manifests)) { if (!manifest) continue; // Convert string resources to StrictManifestResource objects @@ -616,10 +627,15 @@ export class AIOStreams { return resource; }); - const addon = this.addons[Number(index)]; + const addon = this.getAddon(instanceId); + + if (!addon) { + logger.error(`Addon with instanceId ${instanceId} not found`); + continue; + } logger.verbose( - `Determined that ${addon.identifyingName} (Index: ${index}) has support for the following resources: ${JSON.stringify( + `Determined that ${addon.identifyingName} (Instance ID: ${instanceId}) has support for the following resources: ${JSON.stringify( addonResources )}` ); @@ -655,7 +671,7 @@ export class AIOStreams { } } - // Add catalogs with prefixed IDs (ensure to check that if addon.resources is defined and does not have catalog + // Add catalogs with prefixed IDs (ensure to check that if addon.resources is defined and does not have catalog // then we do not add the catalogs) if ( @@ -665,7 +681,7 @@ export class AIOStreams { this.finalCatalogs.push( ...manifest.catalogs.map((catalog) => ({ ...catalog, - id: `${index}.${catalog.id}`, + id: `${addon.instanceId}.${catalog.id}`, })) ); } @@ -675,12 +691,12 @@ export class AIOStreams { this.finalAddonCatalogs!.push( ...(manifest.addonCatalogs || []).map((catalog) => ({ ...catalog, - id: `${index}.${catalog.id}`, + id: `${addon.instanceId}.${catalog.id}`, })) ); } - this.supportedResources[Number(index)] = addonResources; + this.supportedResources[instanceId] = addonResources; } logger.verbose( @@ -780,8 +796,8 @@ export class AIOStreams { return this.finalAddonCatalogs; } - public getAddon(index: number): Addon { - return this.addons[index]; + public getAddon(instanceId: string): Addon | undefined { + return this.addons.find((a) => a.instanceId === instanceId); } private shouldProxyStream(stream: ParsedStream): boolean { @@ -793,7 +809,7 @@ export class AIOStreams { const proxyAddon = !proxy.proxiedAddons?.length || - proxy.proxiedAddons.includes(stream.addon.id || ''); + proxy.proxiedAddons.includes(stream.addon.presetInstanceId); const proxyService = !proxy.proxiedServices?.length || proxy.proxiedServices.includes(streamService); @@ -857,7 +873,9 @@ export class AIOStreams { const proxy = this.userData.proxy && (!this.userData.proxy?.proxiedAddons?.length || - this.userData.proxy.proxiedAddons.includes(addon.id || '')); + this.userData.proxy.proxiedAddons.includes( + addon.presetInstanceId || '' + )); logger.debug( `Using ${proxy ? 'proxy' : 'user'} ip for ${addon.identifyingName}: ${ proxy @@ -876,7 +894,7 @@ export class AIOStreams { private async getStreamsFromAddons(type: string, id: string) { // get a list of all addons that support the stream resource with the given type and id. const supportedAddons = []; - for (const [index, addonResources] of Object.entries( + for (const [instanceId, addonResources] of Object.entries( this.supportedResources )) { const resource = addonResources.find( @@ -888,7 +906,7 @@ export class AIOStreams { : true) // if no id prefixes are defined, assume it supports all IDs ); if (resource) { - const addon = this.getAddon(Number(index)); + const addon = this.getAddon(instanceId); if (addon) { supportedAddons.push(addon); } @@ -1006,7 +1024,8 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` // Always fetch from first group const firstGroupAddons = supportedAddons.filter( (addon) => - addon.id && this.userData.groups![0].addons.includes(addon.id) + addon.presetInstanceId && + this.userData.groups![0].addons.includes(addon.presetInstanceId) ); logger.info( @@ -1039,7 +1058,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` logger.info(`Condition met for group ${i + 1}, fetching streams`); const groupAddons = supportedAddons.filter( - (addon) => addon.id && group.addons.includes(addon.id) + (addon) => + addon.presetInstanceId && + group.addons.includes(addon.presetInstanceId) ); const groupResult = await fetchFromGroup(groupAddons); @@ -1085,12 +1106,12 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` ); } if ( - addon.fromPresetId && - FeatureControl.disabledAddons.has(addon.fromPresetId) + addon.presetInstanceId && + FeatureControl.disabledAddons.has(addon.presetInstanceId) ) { throw new Error( `Addon ${addon.identifyingName} is disabled: ${FeatureControl.disabledAddons.get( - addon.fromPresetId + addon.presetType )}` ); } else if ( @@ -1187,9 +1208,10 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` ) { return true; } + if ( titleMatchingOptions.addons?.length && - !titleMatchingOptions.addons.includes(stream.addon.id!) + !titleMatchingOptions.addons.includes(stream.addon.presetInstanceId) ) { return true; } @@ -1238,7 +1260,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` if ( seasonEpisodeMatchingOptions.addons?.length && - !seasonEpisodeMatchingOptions.addons.includes(stream.addon.id!) + !seasonEpisodeMatchingOptions.addons.includes( + stream.addon.presetInstanceId + ) ) { return true; } @@ -1267,7 +1291,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` }; const excludedRegexPatterns = - isRegexAllowed && this.userData.excludedRegexPatterns + isRegexAllowed && + this.userData.excludedRegexPatterns && + this.userData.excludedRegexPatterns.length > 0 ? await Promise.all( this.userData.excludedRegexPatterns.map( async (pattern) => await compileRegex(pattern) @@ -1276,7 +1302,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` : undefined; const requiredRegexPatterns = - isRegexAllowed && this.userData.requiredRegexPatterns + isRegexAllowed && + this.userData.requiredRegexPatterns && + this.userData.requiredRegexPatterns.length > 0 ? await Promise.all( this.userData.requiredRegexPatterns.map( async (pattern) => await compileRegex(pattern) @@ -1285,7 +1313,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` : undefined; const includedRegexPatterns = - isRegexAllowed && this.userData.includedRegexPatterns + isRegexAllowed && + this.userData.includedRegexPatterns && + this.userData.includedRegexPatterns.length > 0 ? await Promise.all( this.userData.includedRegexPatterns.map( async (pattern) => await compileRegex(pattern) @@ -1337,7 +1367,7 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` const isAddonFilteredOut = addonIds && addonIds.length > 0 && - addonIds.some((addonId) => stream.addon.id === addonId) && + addonIds.some((addonId) => stream.addon.presetInstanceId === addonId) && stream.service?.cached === cached; const isServiceFilteredOut = serviceIds && @@ -1835,6 +1865,7 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` } if ( requiredRegexPatterns && + requiredRegexPatterns.length > 0 && !(await testRegexes(stream, requiredRegexPatterns)) ) { skipReasons.requiredRegex.total++; @@ -2131,11 +2162,11 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` const aAddonIndex = this.userData.presets.findIndex( - (preset) => JSON.stringify(preset) === a.addon.id + (preset) => preset.instanceId === a.addon.presetInstanceId ) ?? -1; const bAddonIndex = this.userData.presets.findIndex( - (preset) => JSON.stringify(preset) === b.addon.id + (preset) => preset.instanceId === b.addon.presetInstanceId ) ?? -1; // the addon index MUST exist, its not possible for it to not exist @@ -2183,11 +2214,11 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` return serviceStreams.sort((a, b) => { const aAddonIndex = this.userData.presets.findIndex( - (preset) => JSON.stringify(preset) === a.addon.id + (preset) => preset.instanceId === a.addon.presetInstanceId ) ?? -1; const bAddonIndex = this.userData.presets.findIndex( - (preset) => JSON.stringify(preset) === b.addon.id + (preset) => preset.instanceId === b.addon.presetInstanceId ) ?? -1; if (aAddonIndex !== bAddonIndex) { return aAddonIndex - bAddonIndex; @@ -2227,8 +2258,9 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` let perAddonStreams = Object.values( typeStreams.reduce( (acc, stream) => { - acc[stream.addon.id!] = acc[stream.addon.id!] || []; - acc[stream.addon.id!].push(stream); + acc[stream.addon.presetInstanceId] = + acc[stream.addon.presetInstanceId] || []; + acc[stream.addon.presetInstanceId].push(stream); return acc; }, {} as Record @@ -2278,6 +2310,7 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` this.userData.preferredRegexPatterns.map(async (pattern) => { return { name: pattern.name, + negate: parseRegex(pattern.pattern).flags.includes('n'), pattern: await compileRegex(pattern.pattern), }; }) @@ -2302,24 +2335,48 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` isMatch(preferredKeywordsPatterns, stream.indexer || ''); }); } + const determineMatch = ( + stream: ParsedStream, + regexPattern: { pattern: RegExp; negate: boolean }, + attribute?: string + ) => { + if (regexPattern.negate) { + return attribute ? !isMatch(regexPattern.pattern, attribute) : true; + } + return attribute ? isMatch(regexPattern.pattern, attribute) : false; + }; if (preferredRegexPatterns) { streams.forEach((stream) => { for (let i = 0; i < preferredRegexPatterns.length; i++) { + // if negate, then the pattern must not match any of the attributes + // and if the attribute is undefined, then we can consider that as a non-match so true const regexPattern = preferredRegexPatterns[i]; - if ( - regexPattern && - !stream.regexMatched && - ((stream.filename && - isMatch(regexPattern.pattern, stream.filename)) || - (stream.folderName && - isMatch(regexPattern.pattern, stream.folderName)) || - (stream.parsedFile?.releaseGroup && - isMatch( - regexPattern.pattern, - stream.parsedFile?.releaseGroup || '' - )) || - (stream.indexer && isMatch(regexPattern.pattern, stream.indexer))) - ) { + const filenameMatch = determineMatch( + stream, + regexPattern, + stream.filename + ); + const folderNameMatch = determineMatch( + stream, + regexPattern, + stream.folderName + ); + const releaseGroupMatch = determineMatch( + stream, + regexPattern, + stream.parsedFile?.releaseGroup + ); + const indexerMatch = determineMatch( + stream, + regexPattern, + stream.indexer + ); + let match = + filenameMatch || + folderNameMatch || + releaseGroupMatch || + indexerMatch; + if (match) { stream.regexMatched = { name: regexPattern.name, pattern: regexPattern.pattern.source, @@ -2452,7 +2509,7 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` case 'addon': // find the first occurence of the stream.addon.id in the addons array const idx = userData.presets.findIndex( - (p) => JSON.stringify(p) === stream.addon.id + (p) => p.instanceId === stream.addon.presetInstanceId ); return multiplier * (idx !== -1 ? -idx : 0); @@ -2671,13 +2728,13 @@ ${errorStreams.length > 0 ? ` ❌ Errors : ${errorStreams.map((s) => ` } // Check addon limit - if (addon && stream.addon.id) { - const count = counts.addon.get(stream.addon.id) || 0; + if (addon) { + const count = counts.addon.get(stream.addon.presetInstanceId) || 0; if (count >= addon) { indexesToRemove.add(index); return; } - counts.addon.set(stream.addon.id, count + 1); + counts.addon.set(stream.addon.presetInstanceId, count + 1); } // Check stream type limit diff --git a/packages/core/src/presets/aiostreams.ts b/packages/core/src/presets/aiostreams.ts index 5868d698..bb7da4d9 100644 --- a/packages/core/src/presets/aiostreams.ts +++ b/packages/core/src/presets/aiostreams.ts @@ -142,7 +142,8 @@ export class AIOStreamsPreset extends Preset { library: false, resources: options.resources || undefined, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/animeKitsu.ts b/packages/core/src/presets/animeKitsu.ts index 3c178c40..45335011 100644 --- a/packages/core/src/presets/animeKitsu.ts +++ b/packages/core/src/presets/animeKitsu.ts @@ -54,7 +54,8 @@ export class AnimeKitsuPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/comet.ts b/packages/core/src/presets/comet.ts index 49d99b5e..518424da 100644 --- a/packages/core/src/presets/comet.ts +++ b/packages/core/src/presets/comet.ts @@ -143,7 +143,8 @@ export class CometPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/custom.ts b/packages/core/src/presets/custom.ts index b9ac170f..496184e4 100644 --- a/packages/core/src/presets/custom.ts +++ b/packages/core/src/presets/custom.ts @@ -99,7 +99,8 @@ export class CustomPreset extends Preset { library: options.libraryAddon ?? false, resources: options.resources || undefined, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', streamPassthrough: options.streamPassthrough ?? false, headers: { 'User-Agent': this.METADATA.USER_AGENT, diff --git a/packages/core/src/presets/dcUniverse.ts b/packages/core/src/presets/dcUniverse.ts index 5a2c8f9d..303171c5 100644 --- a/packages/core/src/presets/dcUniverse.ts +++ b/packages/core/src/presets/dcUniverse.ts @@ -116,7 +116,8 @@ export class DcUniversePreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/debridio.ts b/packages/core/src/presets/debridio.ts index 00805cb1..ee775366 100644 --- a/packages/core/src/presets/debridio.ts +++ b/packages/core/src/presets/debridio.ts @@ -93,7 +93,8 @@ export class DebridioPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/debridioTmdb.ts b/packages/core/src/presets/debridioTmdb.ts index 2bb0a89b..9c0d749a 100644 --- a/packages/core/src/presets/debridioTmdb.ts +++ b/packages/core/src/presets/debridioTmdb.ts @@ -134,7 +134,8 @@ export class DebridioTmdbPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/debridioTv.ts b/packages/core/src/presets/debridioTv.ts index f862a0ef..607b7ce4 100644 --- a/packages/core/src/presets/debridioTv.ts +++ b/packages/core/src/presets/debridioTv.ts @@ -117,7 +117,8 @@ export class DebridioTvPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/debridioTvdb.ts b/packages/core/src/presets/debridioTvdb.ts index 113ddfac..5d6c3d31 100644 --- a/packages/core/src/presets/debridioTvdb.ts +++ b/packages/core/src/presets/debridioTvdb.ts @@ -81,7 +81,8 @@ export class DebridioTvdbPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/debridioWatchtower.ts b/packages/core/src/presets/debridioWatchtower.ts index f28a08d8..a343f33b 100644 --- a/packages/core/src/presets/debridioWatchtower.ts +++ b/packages/core/src/presets/debridioWatchtower.ts @@ -45,8 +45,7 @@ class DebridioWatchtowerStreamParser extends StreamParser { ]) ); } - - parsedStream.filename = undefined; + parsedStream.filename = stream.behaviorHints?.filename; parsedStream.folderName = undefined; parsedStream.message = stream.description?.replace(/\d+p?/g, ''); @@ -149,7 +148,8 @@ export class DebridioWatchtowerPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/dmmCast.ts b/packages/core/src/presets/dmmCast.ts index 10c1dac2..2b48ea36 100644 --- a/packages/core/src/presets/dmmCast.ts +++ b/packages/core/src/presets/dmmCast.ts @@ -135,7 +135,8 @@ export class DMMCastPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/easynews.ts b/packages/core/src/presets/easynews.ts index bf2b958c..1e9c86fc 100644 --- a/packages/core/src/presets/easynews.ts +++ b/packages/core/src/presets/easynews.ts @@ -71,7 +71,8 @@ export class EasynewsPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/jackettio.ts b/packages/core/src/presets/jackettio.ts index bc111ebc..6b8dc7d8 100644 --- a/packages/core/src/presets/jackettio.ts +++ b/packages/core/src/presets/jackettio.ts @@ -125,7 +125,8 @@ export class JackettioPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/marvel.ts b/packages/core/src/presets/marvel.ts index 83990e15..5ea5d9b4 100644 --- a/packages/core/src/presets/marvel.ts +++ b/packages/core/src/presets/marvel.ts @@ -92,7 +92,8 @@ export class MarvelPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/mediafusion.ts b/packages/core/src/presets/mediafusion.ts index 539b1615..9b4ac9b9 100644 --- a/packages/core/src/presets/mediafusion.ts +++ b/packages/core/src/presets/mediafusion.ts @@ -282,7 +282,9 @@ export class MediaFusionPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', + headers: { 'User-Agent': this.METADATA.USER_AGENT, encoded_user_data: this.generateEncodedUserData( diff --git a/packages/core/src/presets/nuviostreams.ts b/packages/core/src/presets/nuviostreams.ts index 04065241..686c8a2c 100644 --- a/packages/core/src/presets/nuviostreams.ts +++ b/packages/core/src/presets/nuviostreams.ts @@ -175,7 +175,8 @@ export class NuvioStreamsPreset extends Preset { streamPassthrough: options.streamPassthrough ?? true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/opensubtitles.ts b/packages/core/src/presets/opensubtitles.ts index e32b9c58..70955959 100644 --- a/packages/core/src/presets/opensubtitles.ts +++ b/packages/core/src/presets/opensubtitles.ts @@ -48,7 +48,8 @@ export class OpenSubtitlesPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/orion.ts b/packages/core/src/presets/orion.ts index fda0fd23..15d2dd61 100644 --- a/packages/core/src/presets/orion.ts +++ b/packages/core/src/presets/orion.ts @@ -137,7 +137,8 @@ export class OrionPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/peerflix.ts b/packages/core/src/presets/peerflix.ts index 8bef8382..a88645b6 100644 --- a/packages/core/src/presets/peerflix.ts +++ b/packages/core/src/presets/peerflix.ts @@ -125,7 +125,8 @@ export class PeerflixPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/presetManager.ts b/packages/core/src/presets/presetManager.ts index c2c36e42..6dec7cf4 100644 --- a/packages/core/src/presets/presetManager.ts +++ b/packages/core/src/presets/presetManager.ts @@ -1,4 +1,4 @@ -import { PresetMetadata } from '../db'; +import { PresetMetadata, PresetMinimalMetadata } from '../db'; import { CometPreset } from './comet'; import { CustomPreset } from './custom'; import { MediaFusionPreset } from './mediafusion'; @@ -64,8 +64,20 @@ const PRESET_LIST: string[] = [ ]; export class PresetManager { - static getPresetList(): PresetMetadata[] { - return PRESET_LIST.map((presetId) => this.fromId(presetId).METADATA); + static getPresetList(): PresetMinimalMetadata[] { + return PRESET_LIST.map((presetId) => this.fromId(presetId).METADATA).map( + (metadata) => ({ + ID: metadata.ID, + NAME: metadata.NAME, + LOGO: metadata.LOGO, + DESCRIPTION: metadata.DESCRIPTION, + URL: metadata.URL, + SUPPORTED_RESOURCES: metadata.SUPPORTED_RESOURCES, + SUPPORTED_STREAM_TYPES: metadata.SUPPORTED_STREAM_TYPES, + SUPPORTED_SERVICES: metadata.SUPPORTED_SERVICES, + OPTIONS: metadata.OPTIONS, + }) + ); } static fromId(id: string) { diff --git a/packages/core/src/presets/rpdbCatalogs.ts b/packages/core/src/presets/rpdbCatalogs.ts index 4c2943b3..763b9ad5 100644 --- a/packages/core/src/presets/rpdbCatalogs.ts +++ b/packages/core/src/presets/rpdbCatalogs.ts @@ -78,7 +78,8 @@ export class RpdbCatalogsPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/starWarsUniverse.ts b/packages/core/src/presets/starWarsUniverse.ts index 8d12d2d5..ec4c4a65 100644 --- a/packages/core/src/presets/starWarsUniverse.ts +++ b/packages/core/src/presets/starWarsUniverse.ts @@ -120,7 +120,8 @@ export class StarWarsUniversePreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/streamfusion.ts b/packages/core/src/presets/streamfusion.ts index 8d88c48f..809694f8 100644 --- a/packages/core/src/presets/streamfusion.ts +++ b/packages/core/src/presets/streamfusion.ts @@ -153,7 +153,8 @@ export class StreamFusionPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/stremthruStore.ts b/packages/core/src/presets/stremthruStore.ts index 40acd586..586268ed 100644 --- a/packages/core/src/presets/stremthruStore.ts +++ b/packages/core/src/presets/stremthruStore.ts @@ -107,7 +107,8 @@ export class StremthruStorePreset extends Preset { library: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/stremthruTorz.ts b/packages/core/src/presets/stremthruTorz.ts index a7828105..8acac92c 100644 --- a/packages/core/src/presets/stremthruTorz.ts +++ b/packages/core/src/presets/stremthruTorz.ts @@ -127,7 +127,8 @@ export class StremthruTorzPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/tmdbCollections.ts b/packages/core/src/presets/tmdbCollections.ts index 6b2df3c2..bf23e047 100644 --- a/packages/core/src/presets/tmdbCollections.ts +++ b/packages/core/src/presets/tmdbCollections.ts @@ -95,7 +95,8 @@ export class TmdbCollectionsPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/torbox.ts b/packages/core/src/presets/torbox.ts index 57ddeb98..321f695c 100644 --- a/packages/core/src/presets/torbox.ts +++ b/packages/core/src/presets/torbox.ts @@ -128,7 +128,8 @@ export class TorboxAddonPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/torrentCatalogs.ts b/packages/core/src/presets/torrentCatalogs.ts index a0e90f35..9e90af57 100644 --- a/packages/core/src/presets/torrentCatalogs.ts +++ b/packages/core/src/presets/torrentCatalogs.ts @@ -50,7 +50,8 @@ export class TorrentCatalogsPreset extends Preset { library: false, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/presets/torrentio.ts b/packages/core/src/presets/torrentio.ts index baba09e4..167f94cb 100644 --- a/packages/core/src/presets/torrentio.ts +++ b/packages/core/src/presets/torrentio.ts @@ -148,7 +148,8 @@ export class TorrentioPreset extends Preset { enabled: true, resources: options.resources || this.METADATA.SUPPORTED_RESOURCES, timeout: options.timeout || this.METADATA.TIMEOUT, - fromPresetId: this.METADATA.ID, + presetType: this.METADATA.ID, + presetInstanceId: '', headers: { 'User-Agent': this.METADATA.USER_AGENT, }, diff --git a/packages/core/src/utils/config.ts b/packages/core/src/utils/config.ts index 936729f3..e8f76724 100644 --- a/packages/core/src/utils/config.ts +++ b/packages/core/src/utils/config.ts @@ -367,22 +367,18 @@ function ensureDecrypted(config: UserData): UserData { if (!service.credentials) continue; for (const [credential, value] of Object.entries(service.credentials)) { service.credentials[credential] = tryDecrypt( - decodeURIComponent(value), + value, `credential ${credential}` ); } } - // Decrypt proxy config if (decryptedConfig.proxy) { decryptedConfig.proxy.credentials = decryptedConfig.proxy.credentials - ? tryDecrypt( - decodeURIComponent(decryptedConfig.proxy.credentials), - 'proxy credentials' - ) + ? tryDecrypt(decryptedConfig.proxy.credentials, 'proxy credentials') : undefined; decryptedConfig.proxy.url = decryptedConfig.proxy.url - ? tryDecrypt(decodeURIComponent(decryptedConfig.proxy.url), 'proxy URL') + ? tryDecrypt(decryptedConfig.proxy.url, 'proxy URL') : undefined; } @@ -422,7 +418,7 @@ function validateService( } function validatePreset(preset: PresetObject) { - const presetMeta = PresetManager.fromId(preset.id).METADATA; + const presetMeta = PresetManager.fromId(preset.type).METADATA; const optionMetas = presetMeta.OPTIONS; @@ -521,9 +517,9 @@ function validateOption( } if (option.forced) { - value = encryptString(option.forced).data; + // option.forced is already encrypted + value = option.forced; } - value = decodeURIComponent(value); if (isEncrypted(value) && decryptValues) { const { success, data, error } = decryptString(value); if (!success) { diff --git a/packages/core/src/utils/crypto.ts b/packages/core/src/utils/crypto.ts index 4da6828d..4364a61e 100644 --- a/packages/core/src/utils/crypto.ts +++ b/packages/core/src/utils/crypto.ts @@ -15,6 +15,25 @@ const logger = createLogger('crypto'); const saltRounds = 10; +function base64UrlSafe(data: string): string { + return Buffer.from(data) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +function fromUrlSafeBase64(data: string): string { + // Add padding if needed + const padding = data.length % 4; + const paddedData = padding ? data + '='.repeat(4 - padding) : data; + + return Buffer.from( + paddedData.replace(/-/g, '+').replace(/_/g, '/'), + 'base64' + ).toString('utf-8'); +} + const compressData = (data: string): Buffer => { return deflateSync(Buffer.from(data, 'utf-8'), { level: 9, @@ -72,8 +91,15 @@ type ErrorResponse = { export type Response = SuccessResponse | ErrorResponse; export function isEncrypted(data: string): boolean { - return data?.startsWith('aioEncrypt:') ?? false; + try { + // parse the data as json + const json = JSON.parse(fromUrlSafeBase64(data)); + return json.type === 'aioEncrypt'; + } catch (error) { + return false; + } } + /** * Encrypts a string using AES-256-CBC encryption, returns a string in the format "iv:encrypted" where * iv and encrypted are url encoded. @@ -90,7 +116,9 @@ export function encryptString(data: string, secretKey?: Buffer): Response { const { iv, data: encrypted } = encryptData(secretKey, compressed); return { success: true, - data: encodeURIComponent(`aioEncrypt:${iv}:${encrypted}`), + data: base64UrlSafe( + JSON.stringify({ iv, encrypted, type: 'aioEncrypt' }) + ), error: null, }; } catch (error: any) { @@ -114,13 +142,12 @@ export function decryptString(data: string, secretKey?: Buffer): Response { secretKey = Buffer.from(Env.SECRET_KEY, 'hex'); } try { - data = decodeURIComponent(data); if (!isEncrypted(data)) { throw new Error('The data was not in an expected encrypted format'); } - const [_, ivHex, encryptedHex] = data.split(':'); - const iv = Buffer.from(ivHex, 'base64'); - const encrypted = Buffer.from(encryptedHex, 'base64'); + const json = JSON.parse(fromUrlSafeBase64(data)); + const iv = Buffer.from(json.iv, 'base64'); + const encrypted = Buffer.from(json.encrypted, 'base64'); const decrypted = decryptData(secretKey, encrypted, iv); const decompressed = decompressData(decrypted); return { diff --git a/packages/core/src/utils/regex.ts b/packages/core/src/utils/regex.ts index 63da2c72..7784e335 100644 --- a/packages/core/src/utils/regex.ts +++ b/packages/core/src/utils/regex.ts @@ -40,12 +40,12 @@ export async function safeRegexTest( return false; } } - +// parses regex and flags, also checks for existence of a custom flag - n - for negate export function parseRegex(pattern: string): { regex: string; flags: string; } { - const regexFormatMatch = /^\/(.+)\/([gimuy]*)$/.exec(pattern); + const regexFormatMatch = /^\/(.+)\/([gimun]*)$/.exec(pattern); return regexFormatMatch ? { regex: regexFormatMatch[1], flags: regexFormatMatch[2] } : { regex: pattern, flags: '' }; @@ -55,7 +55,11 @@ export async function compileRegex( pattern: string, bypassCache: boolean = false ): Promise { - const { regex, flags } = parseRegex(pattern); + let { regex, flags } = parseRegex(pattern); + // the n flag is not to be used when compiling the regex + if (flags.includes('n')) { + flags = flags.replace('n', ''); + } if (bypassCache) { return new RegExp(regex, flags); } diff --git a/packages/core/src/wrapper.ts b/packages/core/src/wrapper.ts index edca32e5..b9f2e0d1 100644 --- a/packages/core/src/wrapper.ts +++ b/packages/core/src/wrapper.ts @@ -160,8 +160,8 @@ export class Wrapper { { type, id }, validator ); - const Parser = this.addon.fromPresetId - ? PresetManager.fromId(this.addon.fromPresetId).getParser() + const Parser = this.addon.presetType + ? PresetManager.fromId(this.addon.presetType).getParser() : StreamParser; const parser = new Parser(this.addon); return streams.map((stream: Stream) => parser.parse(stream)); diff --git a/packages/frontend/src/components/menu/addons.tsx b/packages/frontend/src/components/menu/addons.tsx index 37e2a835..b3f75cad 100644 --- a/packages/frontend/src/components/menu/addons.tsx +++ b/packages/frontend/src/components/menu/addons.tsx @@ -40,7 +40,7 @@ import { LuChevronsDown, LuShuffle, } from 'react-icons/lu'; -import { TbSmartHomeOff } from 'react-icons/tb'; +import { TbSmartHome, TbSmartHomeOff } from 'react-icons/tb'; import { AnimatePresence } from 'framer-motion'; import { PageControls } from '../shared/page-controls'; import Image from 'next/image'; @@ -54,6 +54,14 @@ import { import { MdRefresh } from 'react-icons/md'; import { Alert } from '../ui/alert'; import MarkdownLite from '../shared/markdown-lite'; +import { + Accordion, + AccordionTrigger, + AccordionContent, + AccordionItem, +} from '../ui/accordion'; +import { FaArrowRightLong, FaRankingStar, FaShuffle } from 'react-icons/fa6'; +import { PiStarFill, PiStarBold } from 'react-icons/pi'; interface CatalogModification { id: string; @@ -143,17 +151,31 @@ function Content() { setEditingAddonId(null); setModalOpen(true); } + function getUniqueId() { + // generate a 3 character long hex string, ensuring it doesn't already exist in the user's presets + const id = Math.floor(Math.random() * 0xfff) + .toString(16) + .padStart(3, '0'); + if (userData.presets.some((a) => a.instanceId === id)) { + return getUniqueId(); + } + return id; + } function handleModalSubmit(values: Record) { if (modalMode === 'add' && modalPreset) { // Always add a new preset with default values, never edit const newPreset = { - id: modalPreset.ID, + type: modalPreset.ID, + instanceId: getUniqueId(), enabled: true, options: values.options, }; const newKey = getPresetUniqueKey(newPreset); // Prevent adding if a preset with the same unique key already exists + // dont use instanceId here, as that will always be unique + // only prevent adding the same preset type with the same options + // so we use getPresetUniqueKey here. if (userData.presets.some((a) => getPresetUniqueKey(a) === newKey)) { toast.error('You already have an addon with the same options added.'); setModalOpen(false); @@ -170,7 +192,7 @@ function Content() { setUserData((prev) => ({ ...prev, presets: prev.presets.map((a) => - getPresetUniqueKey(a) === editingAddonId + a.instanceId === editingAddonId ? { ...a, options: values.options } : a ), @@ -186,10 +208,10 @@ function Content() { if (!over) return; if (active.id !== over.id) { const oldIndex = userData.presets.findIndex( - (a) => getPresetUniqueKey(a) === active.id + (a) => a.instanceId === active.id ); const newIndex = userData.presets.findIndex( - (a) => getPresetUniqueKey(a) === over.id + (a) => a.instanceId === over.id ); const newPresets = arrayMove(userData.presets, oldIndex, newIndex); setUserData((prev) => ({ @@ -321,7 +343,7 @@ function Content() { sensors={sensors} > getPresetUniqueKey(a))} + items={userData.presets.map((a) => a.instanceId)} strategy={verticalListSortingStrategy} >
@@ -337,42 +359,39 @@ function Content() {
) : ( - userData.presets.map((addon) => { - const preset = status?.settings?.presets.find( - (p: any) => p.ID === addon.id + userData.presets.map((preset) => { + const presetMetadata = status?.settings?.presets.find( + (p: any) => p.ID === preset.type ); return ( { - setModalPreset(preset); + setModalPreset(presetMetadata); setModalInitialValues({ - options: { ...addon.options }, + options: { ...preset.options }, }); setModalMode('edit'); - setEditingAddonId(getPresetUniqueKey(addon)); + setEditingAddonId(preset.instanceId); setModalOpen(true); }} onRemove={() => { setUserData((prev) => ({ ...prev, presets: prev.presets.filter( - (a) => - getPresetUniqueKey(a) !== - getPresetUniqueKey(addon) + (a) => a.instanceId !== preset.instanceId ), })); }} onToggleEnabled={(v: boolean) => { setUserData((prev) => ({ ...prev, - presets: prev.presets.map((a) => - getPresetUniqueKey(a) === - getPresetUniqueKey(addon) - ? { ...a, enabled: v } - : a + presets: prev.presets.map((p) => + p.instanceId === preset.instanceId + ? { ...p, enabled: v } + : p ), })); }} @@ -477,14 +496,16 @@ function Content() { ); } -// Helper to generate a unique key for a user preset +// Helper to generate a key based on an addons id and options function getPresetUniqueKey(preset: { - id: string; + type: string; + instanceId: string; enabled: boolean; options: Record; }) { + // dont include the unique instanceId return JSON.stringify({ - id: preset.id, + type: preset.type, enabled: preset.enabled, options: preset.options, }); @@ -492,14 +513,14 @@ function getPresetUniqueKey(preset: { // Sortable Addon Item for DND (handles both preset and custom addon) function SortableAddonItem({ - addon, preset, + presetMetadata, onEdit, onRemove, onToggleEnabled, }: { - addon: any; preset: any; + presetMetadata: any; onEdit: () => void; onRemove: () => void; onToggleEnabled: (v: boolean) => void; @@ -512,7 +533,7 @@ function SortableAddonItem({ transition, isDragging, } = useSortable({ - id: getPresetUniqueKey(addon), + id: preset.instanceId, }); const style = { transform: CSS.Transform.toString(transform), @@ -529,12 +550,12 @@ function SortableAddonItem({ />
- {preset.ID === 'custom' ? ( + {presetMetadata.ID === 'custom' ? ( ) : ( {preset.NAME} @@ -542,13 +563,13 @@ function SortableAddonItem({

- {addon.options.name} + {preset.options.name}

@@ -913,12 +934,11 @@ function AddonGroupCard() { return userData.presets .filter((preset) => { - const presetStr = JSON.stringify(preset); - return !presetsInOtherGroups.has(presetStr); + return !presetsInOtherGroups.has(preset.instanceId); }) .map((preset) => ({ label: preset.options.name, - value: JSON.stringify(preset), + value: preset.instanceId, textValue: preset.options.name, })); }; @@ -1056,8 +1076,22 @@ function CatalogSettingsCard() { existingMods.map((mod) => `${mod.id}-${mod.type}`) ); - // Keep existing modifications with their settings - const modifications = [...existingMods]; + // first we need to handle existing modifications, to ensure that they keep their order + const modifications = existingMods.map((eMod) => { + const nMod = response.data!.find( + (c) => c.id === eMod.id && c.type === eMod.type + ); + if (nMod) { + return { + // keep all the existing attributes, except addonName, type, hideable + ...eMod, + addonName: nMod.addonName, + type: nMod.type, + hideable: nMod.hideable, + }; + } + return eMod; + }); // Add new catalogs at the bottom response.data!.forEach((catalog) => { @@ -1069,7 +1103,6 @@ function CatalogSettingsCard() { enabled: true, shuffle: false, rpdb: userData.rpdbApiKey ? true : false, - // Store these properties directly in the modification object hideable: catalog.hideable, addonName: catalog.addonName, }); @@ -1100,19 +1133,6 @@ function CatalogSettingsCard() { } }; - const [editingCatalog, setEditingCatalog] = useState<{ - id: string; - type: string; - name?: string; - shuffle: boolean; - rpdb: boolean; - onlyOnDiscover: boolean; - hideable?: boolean; - addonName?: string; - } | null>(null); - - const [modalOpen, setModalOpen] = useState(false); - const capitalise = (str: string | undefined) => { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); @@ -1192,16 +1212,6 @@ function CatalogSettingsCard() { setIsDragging(true); }; - // const confirmClearConfig = useConfirmationDialog({ - // title: 'Sign Out', - // description: 'Are you sure you want to sign out?', - // onConfirm: () => { - // user.setUserData(null); - // user.setUuid(null); - // user.setPassword(null); - // }, - // }); - const confirmRefreshCatalogs = useConfirmationDialog({ title: 'Refresh Catalogs', description: @@ -1261,25 +1271,12 @@ function CatalogSettingsCard() { )} strategy={verticalListSortingStrategy} > -
+
    {(userData.catalogModifications || []).map( (catalog: CatalogModification) => ( { - setEditingCatalog({ - id: catalog.id, - type: catalog.type, - name: catalog.name, - shuffle: catalog.shuffle ?? false, - rpdb: catalog.rpdb ?? false, - onlyOnDiscover: catalog.onlyOnDiscover ?? false, - hideable: catalog.hideable, - addonName: catalog.addonName, - }); - setModalOpen(true); - }} onToggleEnabled={(enabled) => { setUserData((prev) => ({ ...prev, @@ -1295,98 +1292,11 @@ function CatalogSettingsCard() { /> ) )} -
+ )} - - Edit Catalog: {editingCatalog?.name || editingCatalog?.id} -{' '} - {capitalise(editingCatalog?.type)} - {editingCatalog?.addonName} -
- } - > -
{ - e.preventDefault(); - if (!editingCatalog) return; - - setUserData((prev) => ({ - ...prev, - catalogModifications: prev.catalogModifications?.map((c) => - c.id === editingCatalog.id && c.type === editingCatalog.type - ? { - ...c, - name: editingCatalog.name, - shuffle: editingCatalog.shuffle, - rpdb: editingCatalog.rpdb, - onlyOnDiscover: editingCatalog.onlyOnDiscover, - } - : c - ), - })); - setModalOpen(false); - }} - > -
- { - setEditingCatalog((prev) => (prev ? { ...prev, name } : null)); - }} - /> - - { - setEditingCatalog((prev) => - prev ? { ...prev, shuffle } : null - ); - }} - /> - - { - setEditingCatalog((prev) => (prev ? { ...prev, rpdb } : null)); - }} - /> - {editingCatalog?.hideable && ( - { - setEditingCatalog((prev) => - prev ? { ...prev, onlyOnDiscover } : null - ); - }} - /> - )} -
- -
- ); @@ -1395,12 +1305,10 @@ function CatalogSettingsCard() { // Add the SortableCatalogItem component function SortableCatalogItem({ catalog, - onEdit, onToggleEnabled, capitalise, }: { catalog: CatalogModification; - onEdit: () => void; onToggleEnabled: (enabled: boolean) => void; capitalise: (str: string | undefined) => string; }) { @@ -1451,52 +1359,305 @@ function SortableCatalogItem({ }); }; + const [modalOpen, setModalOpen] = useState(false); + const [newName, setNewName] = useState(catalog.name || ''); + + const dynamicIconSize = `text-xl h-8 w-8 lg:text-2xl lg:h-10 lg:w-10`; + + const handleNameEdit = () => { + setUserData((prev) => ({ + ...prev, + catalogModifications: prev.catalogModifications?.map((c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, name: newName } + : c + ), + })); + setModalOpen(false); + }; + return ( -
-
+
  • +
    + {/* Full-height drag handle - rounded vertical oval with spacing */}
    -
    -

    - {catalog.name ?? catalog.id} - {capitalise(catalog.type)} -

    -
    -
    - {catalog.shuffle && ( - - )} - {catalog.onlyOnDiscover && ( - - )} - - } - intent="primary-subtle" - onClick={onEdit} - /> - } - intent="primary-subtle" - onClick={moveToTop} - /> - } - intent="primary-subtle" - onClick={moveToBottom} - /> + {/* Content wrapper */} +
    + {/* Header section */} +
    +
    +

    + {catalog.addonName} - {catalog.name ?? catalog.id} +

    + } + intent="primary-subtle" + onClick={() => setModalOpen(true)} + /> +
    +

    + {catalog.type} +

    + + {/* Mobile Controls Row - only visible on small screens */} +
    + {/* Position controls - aligned left */} +
    + } + intent="primary-subtle" + onClick={moveToTop} + title="Move to top" + /> + } + intent="primary-subtle" + onClick={moveToBottom} + title="Move to bottom" + /> +
    + + {/* Enable/disable toggle - aligned right */} + +
    + + {/* Desktop Controls - only visible on medium screens and up */} +
    + +
    + } + intent="primary-subtle" + onClick={moveToTop} + title="Move to top" + /> + } + intent="primary-subtle" + onClick={moveToBottom} + title="Move to bottom" + /> +
    +
    +
    + + {/* Settings section */} + + + +
    +

    + Settings +

    + + {/* Active modifier icons */} +
    + + ) : ( + + ) + } + intent="primary-subtle" + rounded + onClick={(e) => { + e.stopPropagation(); + setUserData((prev) => ({ + ...prev, + catalogModifications: + prev.catalogModifications?.map((c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, shuffle: !c.shuffle } + : c + ), + })); + }} + /> + } + > + Shuffle + + : } + intent="primary-subtle" + rounded + onClick={(e) => { + e.stopPropagation(); + setUserData((prev) => ({ + ...prev, + catalogModifications: + prev.catalogModifications?.map((c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, rpdb: !c.rpdb } + : c + ), + })); + }} + /> + } + > + RPDB + + + {catalog.hideable && ( + + ) : ( + + ) + } + intent="primary-subtle" + rounded + onClick={(e) => { + e.stopPropagation(); + setUserData((prev) => ({ + ...prev, + catalogModifications: + prev.catalogModifications?.map((c) => + c.id === catalog.id && + c.type === catalog.type + ? { + ...c, + onlyOnDiscover: !c.onlyOnDiscover, + } + : c + ), + })); + }} + /> + } + > + Discover Only + + )} +
    +
    +
    + +
    + {/* Large screens: horizontal layout, Medium and below: vertical layout */} +
    + { + setUserData((prev) => ({ + ...prev, + catalogModifications: prev.catalogModifications?.map( + (c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, shuffle } + : c + ), + })); + }} + /> + + { + setUserData((prev) => ({ + ...prev, + catalogModifications: prev.catalogModifications?.map( + (c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, rpdb } + : c + ), + })); + }} + /> + + {catalog.hideable && ( + { + setUserData((prev) => ({ + ...prev, + catalogModifications: + prev.catalogModifications?.map((c) => + c.id === catalog.id && c.type === catalog.type + ? { ...c, onlyOnDiscover } + : c + ), + })); + }} + /> + )} +
    +
    +
    +
    +
    -
    + + {/* Name edit modal */} + +
    { + e.preventDefault(); + handleNameEdit(); + }} + > + + + +
    +
  • ); } diff --git a/packages/frontend/src/components/menu/filters.tsx b/packages/frontend/src/components/menu/filters.tsx index 7b2ed6f5..ae6981de 100644 --- a/packages/frontend/src/components/menu/filters.tsx +++ b/packages/frontend/src/components/menu/filters.tsx @@ -333,9 +333,9 @@ function Content() { })); }} options={userData.presets.map((preset) => ({ - label: preset.options.name || preset.id, - value: JSON.stringify(preset), - textValue: preset.options.name || preset.id, + label: preset.options.name || preset.type, + value: preset.instanceId, + textValue: preset.options.name || preset.type, }))} emptyMessage="You haven't installed any addons..." placeholder="Select addons..." @@ -435,8 +435,8 @@ function Content() { })); }} options={userData.presets.map((preset) => ({ - label: preset.options.name, - value: JSON.stringify(preset), + label: preset.options.name || preset.type, + value: preset.instanceId, textValue: preset.options.name, }))} emptyMessage="You haven't installed any addons..." @@ -1192,11 +1192,11 @@ function Content() { disabled={!userData.titleMatching?.enabled} label="Request Types" emptyMessage="There aren't any request types to choose from..." - help="Request types that will use strict title matching. Leave blank to apply to all request types." + help="Request types that will use title matching. Leave blank to apply to all request types." options={TYPES.map((type) => ({ label: type, value: type, - text: type, + textValue: type, }))} value={userData.titleMatching?.requestTypes} onValueChange={(value) => { @@ -1216,9 +1216,9 @@ function Content() { help="Addons that will use strict title matching. Leave blank to apply to all addons." emptyMessage="You haven't installed any addons yet..." options={userData.presets.map((preset) => ({ - label: preset.options.name, - type: preset.options.name, - value: JSON.stringify(preset), + label: preset.options.name || preset.type, + type: preset.options.name || preset.type, + value: preset.instanceId, }))} value={userData.titleMatching?.addons || []} onValueChange={(value) => { @@ -1265,7 +1265,7 @@ function Content() { options={TYPES.map((type) => ({ label: type, value: type, - text: type, + textValue: type, }))} value={userData.seasonEpisodeMatching?.requestTypes} onValueChange={(value) => { @@ -1285,9 +1285,9 @@ function Content() { help="Addons that will use season/episode matching. Leave blank to apply to all addons." emptyMessage="You haven't installed any addons yet..." options={userData.presets.map((preset) => ({ - label: preset.options.name, - type: preset.options.name, - value: JSON.stringify(preset), + label: preset.options.name || preset.type, + type: preset.options.name || preset.type, + value: preset.instanceId, }))} value={userData.seasonEpisodeMatching?.addons || []} onValueChange={(value) => { diff --git a/packages/frontend/src/components/menu/formatter.tsx b/packages/frontend/src/components/menu/formatter.tsx index 3c5f7870..ee073116 100644 --- a/packages/frontend/src/components/menu/formatter.tsx +++ b/packages/frontend/src/components/menu/formatter.tsx @@ -191,6 +191,8 @@ function Content() { addon: { name: addonName, identifyingName: addonName, + presetType: 'custom', + presetInstanceId: '', enabled: true, manifestUrl: 'http://localhost:2000/manifest.json', timeout: 10000, diff --git a/packages/frontend/src/components/menu/proxy.tsx b/packages/frontend/src/components/menu/proxy.tsx index 1f770100..119d520c 100644 --- a/packages/frontend/src/components/menu/proxy.tsx +++ b/packages/frontend/src/components/menu/proxy.tsx @@ -64,8 +64,8 @@ function Content() { const addonOptions = userData.presets.map((preset) => { return { - label: preset.options.name, - value: JSON.stringify(preset), + label: preset.options.name || preset.type, + value: preset.instanceId, textValue: preset.options.name, }; }); diff --git a/packages/server/src/routes/api/catalog.ts b/packages/server/src/routes/api/catalog.ts index 92a800db..a24f12fa 100644 --- a/packages/server/src/routes/api/catalog.ts +++ b/packages/server/src/routes/api/catalog.ts @@ -39,7 +39,7 @@ router.post('/', async (req: Request, res: Response, next: NextFunction) => { id: catalog.id, name: catalog.name, type: catalog.type, - addonName: aio.getAddon(parseInt(catalog.id.split(':')[0])).name, + addonName: aio.getAddon(catalog.id.split('.')[0])?.name, hideable: catalog.extra ? catalog.extra?.findIndex( (extra) =>