feat(addon): support a custom formatter

not configurable in frontend yet
This commit is contained in:
Viren070
2024-12-31 20:18:40 +00:00
parent 8185f5e95b
commit b0f27c8b55
7 changed files with 203 additions and 7 deletions
+11
View File
@@ -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');
}
+15 -6
View File
@@ -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(
+2 -1
View File
@@ -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"
}
}
+139
View File
@@ -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}
}
+1
View File
@@ -2,3 +2,4 @@ export * from './gdrive';
export * from './utils';
export * from './torrentio';
export * from './torbox';
export * from './custom';
+3
View File
@@ -8,6 +8,9 @@
"references": [
{
"path": "../types"
},
{
"path": "../wrappers"
}
]
}
+32
View File
@@ -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;