From b0f27c8b5547fbf1d40f4e1216a6b59da9746af5 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Tue, 31 Dec 2024 20:18:40 +0000 Subject: [PATCH] feat(addon): support a custom formatter not configurable in frontend yet --- packages/addon/src/addon.ts | 11 +++ packages/addon/src/config.ts | 21 +++-- packages/formatters/package.json | 3 +- packages/formatters/src/custom.ts | 139 ++++++++++++++++++++++++++++++ packages/formatters/src/index.ts | 1 + packages/formatters/tsconfig.json | 3 + packages/types/src/types.ts | 32 +++++++ 7 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 packages/formatters/src/custom.ts diff --git a/packages/addon/src/addon.ts b/packages/addon/src/addon.ts index 14120ccb..1d6d8870 100644 --- a/packages/addon/src/addon.ts +++ b/packages/addon/src/addon.ts @@ -10,6 +10,7 @@ import { gdriveFormat, torrentioFormat, torboxFormat, + customFormat, } from '@aiostreams/formatters'; export class AIOStreams { @@ -168,6 +169,16 @@ export class AIOStreams { description = _description; break; } + case 'custom': { + if (!this.config.customFormatter) { + throw new Error('Custom formatter is not defined'); + } + const { name: _name, description: _description } = + customFormat(parsedStream, this.config.customFormatter); + name = _name; + description = _description; + break; + } default: { throw new Error('Unsupported formatter'); } diff --git a/packages/addon/src/config.ts b/packages/addon/src/config.ts index 31d2fa23..45eab4e7 100644 --- a/packages/addon/src/config.ts +++ b/packages/addon/src/config.ts @@ -169,13 +169,22 @@ export function validateConfig(config: Config): { } if (!allowedFormatters.includes(config.formatter)) { - return createResponse( - false, - 'invalidFormatter', - `Invalid formatter: ${config.formatter}` - ); + if (config.formatter === 'custom') { + if (!config.customFormatter) { + return createResponse( + false, + 'missingCustomFormatter', + 'Custom formatter is required if custom formatter is selected' + ); + } + } else { + return createResponse( + false, + 'invalidFormatter', + `Invalid formatter: ${config.formatter}` + ); + } } - for (const service of config.services) { if (service.enabled) { const serviceDetail = serviceDetails.find( diff --git a/packages/formatters/package.json b/packages/formatters/package.json index 3feda569..c3cecffe 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -8,6 +8,7 @@ }, "description": "Library to take parsed information and return a formatted Stremio stream name and description", "dependencies": { - "@aiostreams/types": "^1.0.0" + "@aiostreams/types": "^1.0.0", + "@aiostreams/wrappers": "^1.0.0" } } diff --git a/packages/formatters/src/custom.ts b/packages/formatters/src/custom.ts new file mode 100644 index 00000000..d9ff2a98 --- /dev/null +++ b/packages/formatters/src/custom.ts @@ -0,0 +1,139 @@ +import { Config, ParsedStream } from '@aiostreams/types'; +import { formatSize } from './utils'; +import { serviceDetails } from '@aiostreams/wrappers'; +import { languageToEmoji } from './utils'; + +/* +{ + splitter: string; // e.g. ' - ', ' | ' etc. the string used to split the tags + // we need to have a key for where the value has to be determined from the stream data. + // examples of this is provider and seedersOrAge. provider.cached has different states and seedersOrAge has different states + + // available values: + // {resolution}, {quality}, {streamType} {encode}, {visualTags}, {audioTags}, {languages}, {provider}, {seedersOrAge}, {filename}, {size}, {addonName}, {seedersOrAge} + languages: { + useEmojis: boolean; // e.g. 🇬🇧 + } + hideIfUnknown: boolean; // if true, the tag will not be shown if the value is unknown + provider: { + trueCacheStatus: string; // e.g. ⚡️ + falseCacheStatus: string; // e.g. ⏳ + undefinedCacheStatus: string; // e.g. ❓ + finalString: string; // e.g. [{providerShortName}{cacheStatus}] -> [TB⚡️] or this could be left empty + } + streamType: { + torrent: string; // e.g. 🧲 + usenet: string; // e.g. 📡 + direct: string; // e.g. 📥 + unknown: string; // e.g. ❓ + finalString: string; // e.g. {streamType} -> 🧲 + } + seedersOrAge: { + whenSeeders: string; // e.g. 👤 + whenAge: string; // e.g. 📅 + finalString: string; // e.g. {seedersOrAge} {seedersOrAgeValue} + } + name: string; + description: string; + } +*/ + +export function customFormat(stream: ParsedStream, customFormatter: Config['customFormatter']): { + name: string; + description: string; +} { + + if (!customFormatter) { + throw new Error('Custom formatter not provided'); + } + let name: string = customFormatter.name; + let description: string = customFormatter.description; + + + + if (stream.provider) { + const cacheStatus = stream.provider.cached + ? customFormatter.provider.trueCacheStatus + : stream.provider.cached === undefined + ? customFormatter.provider.undefinedCacheStatus + : customFormatter.provider.falseCacheStatus; + + name = name.replace('{provider}', customFormatter.provider.finalString.replace('{cacheStatus}', cacheStatus).replace('{providerShortName}', stream.provider.name)); + description = description.replace('{provider}', customFormatter.provider.finalString.replace('{cacheStatus}', cacheStatus).replace('{providerShortName}', stream.provider.name)); + } + + if (stream.torrent?.infoHash) { + name = name.replace('{streamType}', customFormatter.streamType.torrent); + description = description.replace('{streamType}', customFormatter.streamType.torrent); + } + + if (stream.usenet) { + name = name.replace('{streamType}', customFormatter.streamType.usenet); + description = description.replace('{streamType}', customFormatter.streamType.usenet); + } + + if (stream.url) { + name = name.replace('{streamType}', customFormatter.streamType.direct); + description = description.replace('{streamType}', customFormatter.streamType.direct); + } + + if (!stream.torrent && !stream.usenet && !stream.url) { + name = name.replace('{streamType}', customFormatter.streamType.unknown); + description = description.replace('{streamType}', customFormatter.streamType.unknown); + } + + name = name.replace('{addonName}', stream.addonName); + name = name.replace('{resolution}', stream.resolution); + name = name.replace('{quality}', stream.quality); + name = name.replace('{encode}', stream.encode); + name = name.replace('{visualTags}', stream.visualTags.join(customFormatter.splitter)); + name = name.replace('{audioTags}', stream.audioTags.join(customFormatter.splitter)); + name = name.replace('{filename}', stream.filename || 'Unknown Name'); + name = name.replace('{size}', formatSize(stream.size || 0)); + name = name.replace(/\\n/g, '\n'); + + description = description.replace('{addonName}', stream.addonName); + description = description.replace('{resolution}', stream.resolution); + description = description.replace('{quality}', stream.quality); + description = description.replace('{encode}', stream.encode); + description = description.replace('{visualTags}', stream.visualTags.join(customFormatter.splitter)); + description = description.replace('{audioTags}', stream.audioTags.join(customFormatter.splitter)); + description = description.replace('{filename}', stream.filename || 'Unknown Name'); + description = description.replace('{size}', formatSize(stream.size || 0)); + description = description.replace(/\\n/g, '\n'); + + if (stream.torrent?.seeders) { + const seedersStr = customFormatter.seedersOrAge.finalString.replace('{seedersOrAge}', customFormatter.seedersOrAge.whenSeeders).replace('{seedersOrAgeValue}', stream.torrent.seeders.toString()); + name = name.replace('{seedersOrAge}', seedersStr); + description = description.replace('{seedersOrAge}', seedersStr); + } else { + name = name.replace('{seedersOrAge}', ''); + description = description.replace('{seedersOrAge}', ''); + } + + if (stream.usenet?.age) { + const ageStr = customFormatter.seedersOrAge.finalString.replace('{seedersOrAge}', customFormatter.seedersOrAge.whenAge).replace('{seedersOrAgeValue}', stream.usenet.age); + name = name.replace('{seedersOrAge}', ageStr); + description = description.replace('{seedersOrAge}', ageStr); + } else { + name = name.replace('{seedersOrAge}', ''); + description = description.replace('{seedersOrAge}', ''); + } + + + if (customFormatter.languages.useEmojis) { + description = description.replace('{languages}', stream.languages.map((lang) => { + const emoji = languageToEmoji(lang); + return emoji ? emoji : lang; + }).join(customFormatter.splitter)); + name = name.replace('{languages}', stream.languages.map((lang) => { + const emoji = languageToEmoji(lang); + return emoji ? emoji : lang; + }).join(customFormatter.splitter)); + } else { + description = description.replace('{languages}', stream.languages.join(customFormatter.splitter)); + name = name.replace('{languages}', stream.languages.join(customFormatter.splitter)); + } + + return {name, description} +} \ No newline at end of file diff --git a/packages/formatters/src/index.ts b/packages/formatters/src/index.ts index 6a97bf69..6191643f 100644 --- a/packages/formatters/src/index.ts +++ b/packages/formatters/src/index.ts @@ -2,3 +2,4 @@ export * from './gdrive'; export * from './utils'; export * from './torrentio'; export * from './torbox'; +export * from './custom'; \ No newline at end of file diff --git a/packages/formatters/tsconfig.json b/packages/formatters/tsconfig.json index c27709b5..ce1593ce 100644 --- a/packages/formatters/tsconfig.json +++ b/packages/formatters/tsconfig.json @@ -8,6 +8,9 @@ "references": [ { "path": "../types" + }, + { + "path": "../wrappers" } ] } diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 38bba990..485b13c1 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -104,6 +104,38 @@ export interface Config { onlyShowCachedStreams: boolean; prioritiseLanguage: string | null; formatter: string; + customFormatter?: { + splitter: string; // e.g. ' - ', ' | ' etc. the string used to split the tags + // we need to have a key for where the value has to be determined from the stream data. + // examples of this is provider and seedersOrAge. provider.cached has different states and seedersOrAge has different states + + // available values: + // {resolution}, {quality}, {streamType} {encode}, {visualTags}, {audioTags}, {languages}, {provider}, {seedersOrAge}, {filename}, {size}, {addonName}, {seedersOrAge} + languages: { + useEmojis: boolean; // e.g. 🇬🇧 + } + hideIfUnknown: boolean; // if true, the tag will not be shown if the value is unknown + provider: { + trueCacheStatus: string; // e.g. ⚡️ + falseCacheStatus: string; // e.g. ⏳ + undefinedCacheStatus: string; // e.g. ❓ + finalString: string; // e.g. [{providerShortName}{cacheStatus}] -> [TB⚡️] or this could be left empty + } + streamType: { + torrent: string; // e.g. 🧲 + usenet: string; // e.g. 📡 + direct: string; // e.g. 📥 + unknown: string; // e.g. ❓ + finalString: string; // e.g. {streamType} -> 🧲 + } + seedersOrAge: { + whenSeeders: string; // e.g. 👤 + whenAge: string; // e.g. 📅 + finalString: string; // e.g. {seedersOrAge} {seedersOrAgeValue} + } + name: string; + description: string; + } maxSize?: number | null; minSize?: number | null; maxMovieSize: number | null;