brunner56's picture
implement app
0bfe2e3
import {
Option,
Resource,
Stream,
ParsedStream,
UserData,
PresetMetadata,
Addon,
} from '../db';
import { StreamParser } from '../parser';
import { Env, ServiceId, constants } from '../utils';
/**
*
* What modifications are needed for each preset:
*
* comet: apply FORCE_COMET_HOSTNAME, FORCE_COMET_PORT, FORCE_COMET_PROTOCOl to stream urls if they are defined
* dmm cast: need to split title by newline, replace trailing dashes, excluding lines with box emoji, and
* then joining the array back together.
* easynews,easynews+,easynews++: need to set type as usenet
* jackettio: apply FORCE_JACKETTIO_HOSTNAME, FORCE_JACKETTIO_PORT, FORCE_JACKETTIO_PROTOCOL to stream urls if they are defined
* mediafusion: need to add hint for folder name, 📁 emoji, and split on arrow, take last index.
* stremio-jacektt: need to inspect stream urls to extract service info.
* stremthruStore: need to mark each stream as 'inLibrary' and unset any parsed 'indexer'
* torbox: need to use different regex for probably everything.
* torrentio: extract folder name from first line
*/
// name: z.string().min(1),
// enabled: z.boolean().optional(),
// baseUrl: z.string().url().optional(),
// timeout: z.number().min(1).optional(),
// resources: ResourceList.optional(),
export const baseOptions = (
name: string,
resources: Resource[],
timeout: number = Env.DEFAULT_TIMEOUT
): Option[] => [
{
id: 'name',
name: 'Name',
description: 'What to call this addon',
type: 'string',
required: true,
default: name,
},
{
id: 'timeout',
name: 'Timeout',
description: 'The timeout for this addon',
type: 'number',
required: true,
default: timeout,
constraints: {
min: Env.MIN_TIMEOUT,
max: Env.MAX_TIMEOUT,
},
},
{
id: 'resources',
name: 'Resources',
description: 'Optionally override the resources to use ',
type: 'multi-select',
required: false,
default: resources,
options: resources.map((resource) => ({
label: resource,
value: resource,
})),
},
{
id: 'url',
name: 'URL',
description:
'Optionally override either the manifest generated, or override the base url used when generating the manifests',
type: 'url',
required: false,
emptyIsUndefined: true,
default: undefined,
},
];
export abstract class Preset {
static get METADATA(): PresetMetadata {
throw new Error('METADATA must be implemented by derived classes');
}
static getParser(): typeof StreamParser {
return StreamParser;
}
/**
* Creates a preset from a preset id.
* @param presetId - The id of the preset to create.
* @returns The preset.
*/
static generateAddons(
userData: UserData,
options: Record<string, any>
): Promise<Addon[]> {
throw new Error('generateAddons must be implemented by derived classes');
}
// Utility functions for generating config strings
/**
* Encodes a JSON object into a base64 encoded string.
* @param json - The JSON object to encode.
* @returns The base64 encoded string.
*/
protected static base64EncodeJSON(
json: any,
urlEncode: boolean = false, // url encode the string
makeUrlSafe: boolean = false // replace + with -, / with _ and = with nothing
) {
let encoded = Buffer.from(JSON.stringify(json)).toString('base64');
if (makeUrlSafe) {
encoded = encoded
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
} else if (urlEncode) {
encoded = encodeURIComponent(encoded);
}
return encoded;
}
protected static urlEncodeJSON(json: any) {
return encodeURIComponent(JSON.stringify(json));
}
/**
* Transforms key-value pairs into a url encoded string
* @param options - The key-value pair object to encode.
* @returns The encoded string.
*/
protected static urlEncodeKeyValuePairs(
options: Record<string, string> | string[][],
separator: string = '|',
encode: boolean = true
) {
const string = (Array.isArray(options) ? options : Object.entries(options))
.map(([key, value]) => `${key}=${value}`)
.join(separator);
return encode ? encodeURIComponent(string) : string;
}
protected static getUsableServices(
userData: UserData,
specifiedServices?: ServiceId[]
) {
let usableServices = userData.services?.filter(
(service) =>
this.METADATA.SUPPORTED_SERVICES.includes(service.id) && service.enabled
);
if (specifiedServices) {
// Validate specified services exist and are enabled
for (const service of specifiedServices) {
const userService = userData.services?.find((s) => s.id === service);
const meta = Object.values(constants.SERVICE_DETAILS).find(
(s) => s.id === service
);
if (!userService || !userService.enabled || !userService.credentials) {
throw new Error(
`You have specified ${meta?.name || service} in your configuration, but it is not enabled or has missing credentials`
);
}
}
// Filter to only specified services
usableServices = usableServices?.filter((service) =>
specifiedServices.includes(service.id)
);
}
return usableServices;
}
protected static getServiceCredential(
serviceId: ServiceId,
userData: UserData,
specialCases?: Partial<Record<ServiceId, (credentials: any) => any>>
) {
const service = constants.SERVICE_DETAILS[serviceId];
if (!service) {
throw new Error(`Service ${serviceId} not found`);
}
const serviceCredentials = userData.services?.find(
(service) => service.id === serviceId
)?.credentials;
if (!serviceCredentials) {
throw new Error(`No credentials found for service ${serviceId}`);
}
// Handle special cases if provided
if (specialCases?.[serviceId]) {
return specialCases[serviceId](serviceCredentials);
}
// handle seedr
if (serviceId === constants.SEEDR_SERVICE) {
if (serviceCredentials.encodedToken) {
return serviceCredentials.encodedToken;
}
throw new Error(
`Missing encoded token for ${serviceId}. Please add an encoded token using MediaFusion`
);
}
// handle easynews
if (serviceId === constants.EASYNEWS_SERVICE) {
if (!serviceCredentials.username || !serviceCredentials.password) {
throw new Error(
`Missing username or password for ${serviceId}. Please add a username and password.`
);
}
return {
username: serviceCredentials.username,
password: serviceCredentials.password,
};
}
if (serviceId === constants.PIKPAK_SERVICE) {
if (!serviceCredentials.email || !serviceCredentials.password) {
throw new Error(
`Missing email or password for ${serviceId}. Please add an email and password.`
);
}
return {
email: serviceCredentials.email,
password: serviceCredentials.password,
};
}
// Default case - API key
const { apiKey } = serviceCredentials;
if (!apiKey) {
throw new Error(
`Missing credentials for ${serviceId}. Please add an API key.`
);
}
return apiKey;
}
}