import { Option } from '../db'; export enum ErrorCode { // User API USER_NOT_FOUND = 'USER_NOT_FOUND', USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS', USER_INVALID_PASSWORD = 'USER_INVALID_PASSWORD', USER_INVALID_CONFIG = 'USER_INVALID_CONFIG', USER_ERROR = 'USER_ERROR', USER_NEW_PASSWORD_TOO_SHORT = 'USER_NEW_PASSWORD_TOO_SHORT', USER_NEW_PASSWORD_TOO_SIMPLE = 'USER_NEW_PASSWORD_TOO_SIMPLE', // Format API FORMAT_INVALID_FORMATTER = 'FORMAT_INVALID_FORMATTER', FORMAT_INVALID_STREAM = 'FORMAT_INVALID_STREAM', FORMAT_ERROR = 'FORMAT_ERROR', // Other MISSING_REQUIRED_FIELDS = 'MISSING_REQUIRED_FIELDS', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED', RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', } interface ErrorDetails { statusCode: number; message: string; } export const ErrorMap: Record = { [ErrorCode.MISSING_REQUIRED_FIELDS]: { statusCode: 400, message: 'Required fields are missing', }, [ErrorCode.USER_NOT_FOUND]: { statusCode: 404, message: 'User not found', }, [ErrorCode.USER_ALREADY_EXISTS]: { statusCode: 409, message: 'User already exists', }, [ErrorCode.USER_INVALID_PASSWORD]: { statusCode: 401, message: 'Invalid password', }, [ErrorCode.USER_INVALID_CONFIG]: { statusCode: 400, message: 'The config for this user is invalid', }, [ErrorCode.USER_ERROR]: { statusCode: 500, message: 'A generic error while processing the user request', }, [ErrorCode.USER_NEW_PASSWORD_TOO_SHORT]: { statusCode: 400, message: 'New password is too short', }, [ErrorCode.USER_NEW_PASSWORD_TOO_SIMPLE]: { statusCode: 400, message: 'New password is too simple', }, [ErrorCode.INTERNAL_SERVER_ERROR]: { statusCode: 500, message: 'An unexpected error occurred', }, [ErrorCode.METHOD_NOT_ALLOWED]: { statusCode: 405, message: 'Method not allowed', }, [ErrorCode.RATE_LIMIT_EXCEEDED]: { statusCode: 429, message: 'Too many requests from this IP, please try again later.', }, [ErrorCode.FORMAT_INVALID_FORMATTER]: { statusCode: 400, message: 'Invalid formatter', }, [ErrorCode.FORMAT_INVALID_STREAM]: { statusCode: 400, message: 'Invalid stream', }, [ErrorCode.FORMAT_ERROR]: { statusCode: 500, message: 'An error occurred while formatting the stream', }, }; export class APIError extends Error { constructor( public code: ErrorCode, public statusCode: number = ErrorMap[code].statusCode, message?: string ) { super(message || ErrorMap[code].message); this.name = 'APIError'; } } const HEADERS_FOR_IP_FORWARDING = [ 'X-Client-IP', 'X-Forwarded-For', 'X-Real-IP', 'True-Client-IP', 'X-Forwarded', 'Forwarded-For', ]; const API_VERSION = 1; export const GDRIVE_FORMATTER = 'gdrive'; export const LIGHT_GDRIVE_FORMATTER = 'lightgdrive'; export const MINIMALISTIC_GDRIVE_FORMATTER = 'minimalisticgdrive'; export const TORRENTIO_FORMATTER = 'torrentio'; export const TORBOX_FORMATTER = 'torbox'; export const CUSTOM_FORMATTER = 'custom'; export const FORMATTERS = [ GDRIVE_FORMATTER, LIGHT_GDRIVE_FORMATTER, MINIMALISTIC_GDRIVE_FORMATTER, TORRENTIO_FORMATTER, TORBOX_FORMATTER, CUSTOM_FORMATTER, ] as const; export type FormatterDetail = { id: FormatterType; name: string; description: string; }; export const FORMATTER_DETAILS: Record = { [GDRIVE_FORMATTER]: { id: GDRIVE_FORMATTER, name: 'Google Drive', description: 'Uses the formatting from the Stremio GDrive addon', }, [LIGHT_GDRIVE_FORMATTER]: { id: LIGHT_GDRIVE_FORMATTER, name: 'Light Google Drive', description: 'A lighter version of the GDrive formatter, focused on asthetics', }, [MINIMALISTIC_GDRIVE_FORMATTER]: { id: MINIMALISTIC_GDRIVE_FORMATTER, name: 'Minimalistic Google Drive', description: 'A minimalistic formatter for Google Drive which shows only the bare minimum', }, [TORRENTIO_FORMATTER]: { id: TORRENTIO_FORMATTER, name: 'Torrentio', description: 'Uses the formatting from the Torrentio addon', }, [TORBOX_FORMATTER]: { id: TORBOX_FORMATTER, name: 'Torbox', description: 'Uses the formatting from the TorBox Stremio addon', }, [CUSTOM_FORMATTER]: { id: CUSTOM_FORMATTER, name: 'Custom', description: 'Define your own formatter', }, }; export type FormatterType = (typeof FORMATTERS)[number]; const REALDEBRID_SERVICE = 'realdebrid'; const DEBRIDLINK_SERVICE = 'debridlink'; const PREMIUMIZE_SERVICE = 'premiumize'; const ALLEDEBRID_SERVICE = 'alldebrid'; const TORBOX_SERVICE = 'torbox'; const EASYDEBRID_SERVICE = 'easydebrid'; const PUTIO_SERVICE = 'putio'; const PIKPAK_SERVICE = 'pikpak'; const OFFCLOUD_SERVICE = 'offcloud'; const SEEDR_SERVICE = 'seedr'; const EASYNEWS_SERVICE = 'easynews'; const SERVICES = [ REALDEBRID_SERVICE, DEBRIDLINK_SERVICE, PREMIUMIZE_SERVICE, ALLEDEBRID_SERVICE, TORBOX_SERVICE, EASYDEBRID_SERVICE, PUTIO_SERVICE, PIKPAK_SERVICE, OFFCLOUD_SERVICE, SEEDR_SERVICE, EASYNEWS_SERVICE, ] as const; export type ServiceId = (typeof SERVICES)[number]; export const MEDIAFLOW_SERVICE = 'mediaflow' as const; export const STREMTHRU_SERVICE = 'stremthru' as const; export const PROXY_SERVICES = [MEDIAFLOW_SERVICE, STREMTHRU_SERVICE] as const; export type ProxyServiceId = (typeof PROXY_SERVICES)[number]; export const PROXY_SERVICE_DETAILS: Record< ProxyServiceId, { id: ProxyServiceId; name: string; description: string; credentialDescription: string; } > = { [MEDIAFLOW_SERVICE]: { id: MEDIAFLOW_SERVICE, name: 'MediaFlow Proxy', description: '[MediaFlow Proxy](https://github.com/mhdzumair/mediaflow-proxy) is a high performance proxy server which supports HTTP, HLS, and more.', credentialDescription: 'The value of your MediaFlow Proxy instance `API_PASSWORD` environment variable.', }, [STREMTHRU_SERVICE]: { id: STREMTHRU_SERVICE, name: 'StremThru', description: '[StremThru](https://github.com/MunifTanjim/stremthru) is a feature packed companion to Stremio which also offers a HTTP proxy, written in Go.', credentialDescription: 'A valid credential for your StremThru instance, defined in the `STREMTHRU_PROXY_AUTH` environment variable.', }, }; const SERVICE_DETAILS: Record< ServiceId, { id: ServiceId; name: string; shortName: string; knownNames: string[]; signUpText: string; credentials: Option[]; } > = { [REALDEBRID_SERVICE]: { id: REALDEBRID_SERVICE, name: 'Real-Debrid', shortName: 'RD', knownNames: ['RD', 'Real Debrid', 'RealDebrid', 'Real-Debrid'], signUpText: "Don't have an account? [Sign up here](https://real-debrid.com/?id=9483829)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'The API key for the Real-Debrid service. Obtain it from [here](https://real-debrid.com/apitoken)', type: 'password', required: true, }, ], }, [ALLEDEBRID_SERVICE]: { id: ALLEDEBRID_SERVICE, name: 'AllDebrid', shortName: 'AD', knownNames: ['AD', 'All Debrid', 'AllDebrid', 'All-Debrid'], signUpText: "Don't have an account? [Sign up here](https://alldebrid.com/?uid=3n8qa&lang=en)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'The API key for the All-Debrid service. Create one [here](https://alldebrid.com/apikeys)', type: 'password', required: true, }, ], }, [PREMIUMIZE_SERVICE]: { id: PREMIUMIZE_SERVICE, name: 'Premiumize', shortName: 'PM', knownNames: ['PM', 'Premiumize'], signUpText: "Don't have an account? [Sign up here](https://www.premiumize.me/register)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'Your Premiumize API key. Obtain it from [here](https://www.premiumize.me/account)', type: 'password', required: true, }, ], }, [DEBRIDLINK_SERVICE]: { id: DEBRIDLINK_SERVICE, name: 'Debrid-Link', shortName: 'DL', knownNames: ['DL', 'Debrid Link', 'DebridLink', 'Debrid-Link'], signUpText: "Don't have an account? [Sign up here](https://debrid-link.com/id/EY0JO)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'Your Debrid-Link API key. Obtain it from [here](https://debrid-link.com/webapp/apikey)', type: 'password', required: true, }, ], }, [TORBOX_SERVICE]: { id: TORBOX_SERVICE, name: 'TorBox', shortName: 'TB', knownNames: ['TB', 'TorBox', 'Torbox', 'TRB'], signUpText: "Don't have an account? [Sign up here](https://torbox.app/subscription?referral=9ca21adb-dbcb-4fb0-9195-412a5f3519bc) or use my referral code `9ca21adb-dbcb-4fb0-9195-412a5f3519bc`.", credentials: [ { id: 'apiKey', name: 'API Key', description: 'Your Torbox API key. Obtain it from [here](https://torbox.app/settings)', type: 'password', required: true, }, ], }, [OFFCLOUD_SERVICE]: { id: OFFCLOUD_SERVICE, name: 'Offcloud', shortName: 'OC', knownNames: ['OC', 'Offcloud'], signUpText: "Don't have an account? [Sign up here](https://offcloud.com/?=06202a3d)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'Your Offcloud API key. Obtain it from [here](https://offcloud.com/#/account) on the `API Key` tab. ', type: 'password', required: true, }, { id: 'email', name: 'Email', description: 'Your Offcloud email. (These credentials are necessary for some addons)', type: 'password', required: true, }, { id: 'password', name: 'Password', description: 'Your Offcloud password. (These credentials are necessary for some addons)', type: 'password', required: true, }, ], }, [PUTIO_SERVICE]: { id: PUTIO_SERVICE, name: 'put.io', shortName: 'P.IO', knownNames: ['PO', 'put.io', 'putio'], signUpText: "Don't have an account? [Sign up here](https://put.io/)", credentials: [ { id: 'clientId', name: 'Client ID', description: 'Your put.io Client ID. Obtain it from [here](https://app.put.io/oauth)', type: 'password', required: true, }, { id: 'token', name: 'Token', description: 'Your put.io Token. Obtain it from [here](https://app.put.io/oauth)', type: 'password', required: true, }, ], }, [EASYNEWS_SERVICE]: { id: EASYNEWS_SERVICE, name: 'Easynews', shortName: 'EN', knownNames: ['EN', 'Easynews'], signUpText: "Don't have an account? [Sign up here](https://www.easynews.com/)", credentials: [ { id: 'username', name: 'Username', description: 'Your Easynews username', type: 'password', required: true, }, { id: 'password', name: 'Password', description: 'Your Easynews password', type: 'password', required: true, }, ], }, [EASYDEBRID_SERVICE]: { id: EASYDEBRID_SERVICE, name: 'EasyDebrid', shortName: 'ED', knownNames: ['ED', 'EasyDebrid'], signUpText: "Don't have an account? [Sign up here](https://paradise-cloud.com/products/easydebrid)", credentials: [ { id: 'apiKey', name: 'API Key', description: 'Your EasyDebrid API key. Obtain it from [here](https://paradise-cloud.com/dashboard/)', type: 'password', required: true, }, ], }, [PIKPAK_SERVICE]: { id: PIKPAK_SERVICE, name: 'PikPak', shortName: 'PKP', knownNames: ['PP', 'PikPak', 'PKP'], signUpText: "Don't have an account? [Sign up here](https://mypikpak.com/drive/activity/invited?invitation-code=72822731)", credentials: [ { id: 'email', name: 'Email', description: 'Your PikPak email address', type: 'password', required: true, }, { id: 'password', name: 'Password', description: 'Your PikPak password', type: 'password', required: true, }, ], }, [SEEDR_SERVICE]: { id: SEEDR_SERVICE, name: 'Seedr', shortName: 'SDR', knownNames: ['SR', 'Seedr', 'SDR'], signUpText: "Don't have an account? [Sign up here](https://www.seedr.cc/?r=6542079)", credentials: [ { id: 'apiKey', name: 'Encoded Token', description: 'Please authorise at MediaFusion and copy the token into here.', type: 'password', required: true, }, ], }, }; export const DEDUPLICATOR_KEYS = [ 'filename', 'infoHash', 'smartDetect', ] as const; const RESOLUTIONS = [ '2160p', '1440p', '1080p', '720p', '576p', '480p', '360p', '240p', '144p', 'Unknown', ] as const; const QUALITIES = [ 'BluRay REMUX', 'BluRay', 'WEB-DL', 'WEBRip', 'HDRip', 'HC HD-Rip', 'DVDRip', 'HDTV', 'CAM', 'TS', 'TC', 'SCR', 'Unknown', ] as const; const VISUAL_TAGS = [ 'HDR+DV', 'HDR10+', 'HDR10', 'DV', 'HDR', '10bit', '3D', 'IMAX', 'AI', 'SDR', 'Unknown', ] as const; const AUDIO_TAGS = [ 'Atmos', 'DD+', 'DD', 'DTS-HD MA', 'DTS-HD', 'DTS-ES', 'DTS', 'TrueHD', 'OPUS', 'FLAC', 'AAC', 'Unknown', ] as const; const AUDIO_CHANNELS = ['2.0', '5.1', '6.1', '7.1', 'Unknown'] as const; const ENCODES = [ 'AV1', 'HEVC', 'AVC', 'XviD', 'DivX', 'H-OU', 'H-SBS', 'Unknown', ] as const; const SORT_CRITERIA = [ 'quality', 'resolution', 'language', 'visualTag', 'audioTag', 'audioChannel', 'streamType', 'encode', 'size', 'service', 'seeders', 'addon', 'regexPatterns', 'cached', 'library', 'keyword', 'streamExpressionMatched', ] as const; export const MIN_SIZE = 0; export const MAX_SIZE = 100 * 1000 * 1000 * 1000; // 100GB export const MIN_SEEDERS = 0; export const MAX_SEEDERS = 1000; export const DEFAULT_POSTERS = [ 'aHR0cHM6Ly93d3cucG5nbWFydC5jb20vZmlsZXMvMTEvUmlja3JvbGxpbmctUE5HLVBpYy5wbmc=', ]; export const DEFAULT_YT_ID = 'eHZGWmpvNVBnRzA='; export const SORT_CRITERIA_DETAILS = { quality: { name: 'Quality', description: 'Sort by the quality of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred quality list are preferred', descendingDescription: 'Streams that are in your preferred quality list are preferred', }, resolution: { name: 'Resolution', description: 'Sort by the resolution of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred resolution list are preferred', descendingDescription: 'Streams that are in your preferred resolution list are preferred', }, language: { name: 'Language', description: 'Sort by the language of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred language list are preferred', descendingDescription: 'Streams that are in your preferred language list are preferred', }, visualTag: { name: 'Visual Tag', description: 'Sort by the visual tags of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred visual tag list are preferred', descendingDescription: 'Streams that are in your preferred visual tag list are preferred', }, audioTag: { name: 'Audio Tag', description: 'Sort by the audio tags of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred audio tag list are preferred', descendingDescription: 'Streams that are in your preferred audio tag list are preferred', }, audioChannel: { name: 'Audio Channel', description: 'Sort by the audio channels of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred audio channel list are preferred', descendingDescription: 'Streams that are in your preferred audio channel list are preferred', }, streamType: { name: 'Stream Type', description: 'Whether the stream is of a preferred stream type', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred stream type list are preferred', descendingDescription: 'Streams that are in your preferred stream type list are preferred', }, encode: { name: 'Encode', description: 'Whether the stream is of a preferred encode', defaultDirection: 'desc', ascendingDescription: 'Streams that are not in your preferred encode list are preferred', descendingDescription: 'Streams that are in your preferred encode list are preferred', }, size: { name: 'Size', description: 'Sort by the size of the stream', defaultDirection: 'desc', ascendingDescription: 'Streams that are smaller are sorted first', descendingDescription: 'Streams that are larger are sorted first', }, service: { name: 'Service', description: 'Sort by the service order', defaultDirection: 'desc', ascendingDescription: 'Streams without a service are preferred', descendingDescription: 'Streams are ordered by the order of your service list, with non-service streams at the bottom', }, seeders: { name: 'Seeders', description: 'Sort by the number of seeders', defaultDirection: 'desc', ascendingDescription: 'Streams with fewer seeders are preferred', descendingDescription: 'Streams with more seeders are preferred', }, addon: { name: 'Addon', description: 'Sort by the addon order', defaultDirection: 'desc', ascendingDescription: 'Streams are sorted by the order of your addon list', descendingDescription: 'Streams are sorted by the order of your addon list', }, regexPatterns: { name: 'Regex Patterns', description: 'Whether the stream matches any of your preferred regex patterns', defaultDirection: 'desc', ascendingDescription: 'Streams that do not match your preferred regex patterns are preferred', descendingDescription: 'Streams that match your preferred regex patterns are preferred', }, cached: { name: 'Cached', defaultDirection: 'desc', description: 'Whether the stream is cached or not', ascendingDescription: 'Streams that are not cached are preferred', descendingDescription: 'Streams that are cached are preferred', }, library: { name: 'Library', defaultDirection: 'desc', description: 'Whether the stream is in your library (e.g. debrid account) or not', ascendingDescription: 'Streams that are not in your library are preferred', descendingDescription: 'Streams that are in your library are preferred', }, keyword: { name: 'Keyword', defaultDirection: 'desc', description: 'Sort by the keyword of the stream', ascendingDescription: 'Streams that do not match any of your keywords are preferred', descendingDescription: 'Streams that match any of your keywords are preferred', }, streamExpressionMatched: { name: 'Stream Expression Matched', defaultDirection: 'desc', description: 'Whether the stream matches any of your stream expressions', ascendingDescription: 'Streams that do not match your stream expressions are preferred while the ones that do are ranked by the order of your stream expressions', descendingDescription: 'Streams that match your stream expressions are preferred and ranked by the order of your stream expressions', }, } as const; const SORT_DIRECTIONS = ['asc', 'desc'] as const; export const P2P_STREAM_TYPE = 'p2p' as const; export const LIVE_STREAM_TYPE = 'live' as const; export const USENET_STREAM_TYPE = 'usenet' as const; export const DEBRID_STREAM_TYPE = 'debrid' as const; export const HTTP_STREAM_TYPE = 'http' as const; export const EXTERNAL_STREAM_TYPE = 'external' as const; export const YOUTUBE_STREAM_TYPE = 'youtube' as const; export const ERROR_STREAM_TYPE = 'error' as const; const STREAM_TYPES = [ P2P_STREAM_TYPE, LIVE_STREAM_TYPE, USENET_STREAM_TYPE, DEBRID_STREAM_TYPE, HTTP_STREAM_TYPE, EXTERNAL_STREAM_TYPE, YOUTUBE_STREAM_TYPE, ERROR_STREAM_TYPE, ] as const; export type StreamType = (typeof STREAM_TYPES)[number]; const STREAM_RESOURCE = 'stream' as const; const SUBTITLES_RESOURCE = 'subtitles' as const; const CATALOG_RESOURCE = 'catalog' as const; const META_RESOURCE = 'meta' as const; const ADDON_CATALOG_RESOURCE = 'addon_catalog' as const; export const MOVIE_TYPE = 'movie' as const; export const SERIES_TYPE = 'series' as const; export const CHANNEL_TYPE = 'channel' as const; export const TV_TYPE = 'tv' as const; export const ANIME_TYPE = 'anime' as const; export const TYPES = [ MOVIE_TYPE, SERIES_TYPE, CHANNEL_TYPE, TV_TYPE, ANIME_TYPE, ] as const; const RESOURCES = [ STREAM_RESOURCE, SUBTITLES_RESOURCE, CATALOG_RESOURCE, META_RESOURCE, ADDON_CATALOG_RESOURCE, ] as const; const LANGUAGES = [ 'English', 'Japanese', 'Chinese', 'Russian', 'Arabic', 'Portuguese', 'Spanish', 'French', 'German', 'Italian', 'Korean', 'Hindi', 'Bengali', 'Punjabi', 'Marathi', 'Gujarati', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Thai', 'Vietnamese', 'Indonesian', 'Turkish', 'Hebrew', 'Persian', 'Ukrainian', 'Greek', 'Lithuanian', 'Latvian', 'Estonian', 'Polish', 'Czech', 'Slovak', 'Hungarian', 'Romanian', 'Bulgarian', 'Serbian', 'Croatian', 'Slovenian', 'Dutch', 'Danish', 'Finnish', 'Swedish', 'Norwegian', 'Malay', 'Latino', 'Dual Audio', 'Dubbed', 'Multi', 'Unknown', ] as const; export const SNIPPETS = [ { name: 'Year + Season + Episode', description: 'Outputs a nicely formatted year along with the season and episode number', value: '{stream.year::exists["({stream.year}) "||""]}{stream.seasonEpisode::exists["{stream.seasonEpisode::join(\' • \')}"||""]}', }, { name: 'File Size', description: 'Outputs the file size of the stream', value: '{stream.size::>0["{stream.size::bytes}"||""]}', }, { name: 'Duration', description: 'Outputs the duration of the stream', value: '{stream.duration::>0["{stream.duration::time}"||""]}', }, { name: 'P2P marker', description: 'Displays a [P2P] marker if the stream is a P2P stream', value: '{stream.type::=p2p["[P2P]"||""]}', }, { name: 'Languages', description: 'Outputs the languages of the stream. Tip: use stream.languageEmojis if you prefer the flags', value: '{stream.languages::exists["{stream.languages::join(\' • \')}"||""]}', }, ]; export { API_VERSION, SERVICES, RESOLUTIONS, QUALITIES, VISUAL_TAGS, AUDIO_TAGS, AUDIO_CHANNELS, ENCODES, SORT_CRITERIA, SORT_DIRECTIONS, STREAM_TYPES, LANGUAGES, RESOURCES, STREAM_RESOURCE, SUBTITLES_RESOURCE, CATALOG_RESOURCE, META_RESOURCE, ADDON_CATALOG_RESOURCE, REALDEBRID_SERVICE, PREMIUMIZE_SERVICE, ALLEDEBRID_SERVICE, DEBRIDLINK_SERVICE, TORBOX_SERVICE, EASYDEBRID_SERVICE, PUTIO_SERVICE, PIKPAK_SERVICE, OFFCLOUD_SERVICE, SEEDR_SERVICE, EASYNEWS_SERVICE, SERVICE_DETAILS, HEADERS_FOR_IP_FORWARDING, };