import { z } from 'zod'; import * as constants from '../utils/constants'; const ServiceIds = z.enum(constants.SERVICES); const Resolutions = z.enum(constants.RESOLUTIONS); const Qualities = z.enum(constants.QUALITIES); const VisualTags = z.enum(constants.VISUAL_TAGS); const AudioTags = z.enum(constants.AUDIO_TAGS); const AudioChannels = z.enum(constants.AUDIO_CHANNELS); const Encodes = z.enum(constants.ENCODES); // const SortCriteria = z.enum(constants.SORT_CRITERIA); // const SortDirections = z.enum(constants.SORT_DIRECTIONS); const SortCriterion = z.object({ key: z.enum(constants.SORT_CRITERIA), direction: z.enum(constants.SORT_DIRECTIONS), }); export type SortCriterion = z.infer; const StreamTypes = z.enum(constants.STREAM_TYPES); const Languages = z.enum(constants.LANGUAGES); const Formatter = z.object({ id: z.enum(constants.FORMATTERS), definition: z .object({ name: z.string().max(5000), description: z.string().max(5000), }) .optional(), }); const StreamProxyConfig = z.object({ enabled: z.boolean().optional(), id: z.enum(constants.PROXY_SERVICES).optional(), url: z.string().optional(), credentials: z.string().optional(), publicIp: z.string().ip().optional(), proxiedAddons: z.array(z.string().min(1)).optional(), proxiedServices: z.array(z.string().min(1)).optional(), }); export type StreamProxyConfig = z.infer; const ResultLimitOptions = z.object({ global: z.number().min(1).optional(), service: z.number().min(1).optional(), addon: z.number().min(1).optional(), resolution: z.number().min(1).optional(), quality: z.number().min(1).optional(), streamType: z.number().min(1).optional(), indexer: z.number().min(1).optional(), releaseGroup: z.number().min(1).optional(), }); // const SizeFilter = z.object({ // min: z.number().min(1).optional(), // max: z.number().min(1).optional(), // }); const SizeFilter = z.object({ movies: z .tuple([z.number().min(0), z.number().min(0)]) // .object({ // min: z.number().min(1).optional(), // max: z.number().min(1).optional(), // }) .optional(), series: z .tuple([z.number().min(0), z.number().min(0)]) // .object({ // min: z.number().min(1).optional(), // max: z.number().min(1).optional(), // }) .optional(), }); const SizeFilterOptions = z.object({ global: SizeFilter, resolution: z.record(Resolutions, SizeFilter).optional(), }); const ServiceSchema = z.object({ id: ServiceIds, enabled: z.boolean().optional(), credentials: z.record(z.string().min(1), z.string().min(1)), }); export type Service = z.infer; const ServiceList = z.array(ServiceSchema); const ResourceSchema = z.enum(constants.RESOURCES); export type Resource = z.infer; const ResourceList = z.array(ResourceSchema); const AddonSchema = z.object({ 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(), name: z.string(), identifier: z.string().optional(), // true identifier for generating IDs displayIdentifier: z.string().optional(), // identifier for display purposes timeout: z.number().min(1), library: z.boolean().optional(), streamPassthrough: z.boolean().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({ 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 PresetList = z.array(PresetSchema); export type Addon = z.infer; export type Preset = z.infer; const DeduplicatorKey = z.enum(constants.DEDUPLICATOR_KEYS); // deduplicator options. // can choose what keys to use for identifying duplicates. // can choose how duplicates are removed specifically. // we can either // - keep only 1 result from the highest priority service from the highest priority addon (single_result) // - keep 1 result for each enabled service from the higest priority addon (per_service) // - keep 1 result from the highest priority service from each enabled addon (per_addon) const DeduplicatorMode = z.enum([ 'single_result', 'per_service', 'per_addon', 'disabled', ]); const DeduplicatorOptions = z.object({ enabled: z.boolean().optional(), keys: z.array(DeduplicatorKey).optional(), cached: DeduplicatorMode.optional(), uncached: DeduplicatorMode.optional(), p2p: DeduplicatorMode.optional(), http: DeduplicatorMode.optional(), live: DeduplicatorMode.optional(), youtube: DeduplicatorMode.optional(), external: DeduplicatorMode.optional(), }); const OptionDefinition = z.object({ id: z.string().min(1), name: z.string().min(1), description: z.string().min(1), emptyIsUndefined: z.boolean().optional(), type: z.enum([ 'string', 'password', 'number', 'boolean', 'select', 'multi-select', 'url', 'alert', 'socials', ]), required: z.boolean().optional(), default: z.any().optional(), // sensitive: z.boolean().optional(), forced: z.any().optional(), options: z .array( z.object({ value: z.any(), label: z.string().min(1), }) ) .optional(), intent: z .enum([ 'alert', 'info', 'success', 'warning', 'info-basic', 'success-basic', 'warning-basic', 'alert-basic', ]) .optional(), socials: z .array( z.object({ id: z.enum([ 'website', 'github', 'discord', 'ko-fi', 'patreon', 'buymeacoffee', 'github-sponsors', ]), url: z.string().url(), }) ) .optional(), constraints: z .object({ min: z.number().min(1).optional(), // for string inputs, consider this the minimum length. max: z.number().min(1).optional(), // and for number inputs, consider this the minimum and maximum value. }) .optional(), }); export type Option = z.infer; const NameableRegex = z.object({ name: z.string().min(0), pattern: z.string().min(1), }); const Group = z.object({ addons: z.array(z.string().min(1)).min(1), condition: z.string().min(1).max(200), }); export type Group = z.infer; // Resolution, Quality, Encode, Visual Tag, Audio Tag, Stream Type, Keyword, Regex, Cached, Uncached, Size const CatalogModification = z.object({ id: z.string().min(1), // an id that maps to an actual catalog ID type: z.string().min(1), // the type of catalog modification name: z.string().min(1).optional(), // override the name of the catalog shuffle: z.boolean().optional(), // shuffle the catalog persistShuffleFor: z.number().min(0).max(24).optional(), // persist the shuffle for a given amount of time (in hours) onlyOnDiscover: z.boolean().optional(), // only show the catalog on the discover page enabled: z.boolean().optional(), // enable or disable the catalog rpdb: z.boolean().optional(), // use rpdb for posters if supported overrideType: z.string().min(1).optional(), // override the type of the catalog hideable: z.boolean().optional(), // hide the catalog from the home page addonName: z.string().min(1).optional(), // the name of the addon that provides the catalog }); export const UserDataSchema = z.object({ uuid: z.string().uuid().optional(), encryptedPassword: z.string().min(1).optional(), trusted: z.boolean().optional(), addonPassword: z.string().min(1).optional(), ip: z.string().ip().optional(), addonName: z.string().min(1).max(300).optional(), addonLogo: z.string().url().optional(), addonBackground: z.string().url().optional(), addonDescription: z.string().min(1).optional(), excludedResolutions: z.array(Resolutions).optional(), includedResolutions: z.array(Resolutions).optional(), requiredResolutions: z.array(Resolutions).optional(), preferredResolutions: z.array(Resolutions).optional(), excludedQualities: z.array(Qualities).optional(), includedQualities: z.array(Qualities).optional(), requiredQualities: z.array(Qualities).optional(), preferredQualities: z.array(Qualities).optional(), excludedLanguages: z.array(Languages).optional(), includedLanguages: z.array(Languages).optional(), requiredLanguages: z.array(Languages).optional(), preferredLanguages: z.array(Languages).optional(), excludedVisualTags: z.array(VisualTags).optional(), includedVisualTags: z.array(VisualTags).optional(), requiredVisualTags: z.array(VisualTags).optional(), preferredVisualTags: z.array(VisualTags).optional(), excludedAudioTags: z.array(AudioTags).optional(), includedAudioTags: z.array(AudioTags).optional(), requiredAudioTags: z.array(AudioTags).optional(), preferredAudioTags: z.array(AudioTags).optional(), excludedAudioChannels: z.array(AudioChannels).optional(), includedAudioChannels: z.array(AudioChannels).optional(), requiredAudioChannels: z.array(AudioChannels).optional(), preferredAudioChannels: z.array(AudioChannels).optional(), excludedStreamTypes: z.array(StreamTypes).optional(), includedStreamTypes: z.array(StreamTypes).optional(), requiredStreamTypes: z.array(StreamTypes).optional(), preferredStreamTypes: z.array(StreamTypes).optional(), excludedEncodes: z.array(Encodes).optional(), includedEncodes: z.array(Encodes).optional(), requiredEncodes: z.array(Encodes).optional(), preferredEncodes: z.array(Encodes).optional(), excludedRegexPatterns: z.array(z.string().min(1)).optional(), includedRegexPatterns: z.array(z.string().min(1)).optional(), requiredRegexPatterns: z.array(z.string().min(1)).optional(), preferredRegexPatterns: z.array(NameableRegex).optional(), requiredKeywords: z.array(z.string().min(1)).optional(), includedKeywords: z.array(z.string().min(1)).optional(), excludedKeywords: z.array(z.string().min(1)).optional(), preferredKeywords: z.array(z.string().min(1)).optional(), randomiseResults: z.boolean().optional(), enhanceResults: z.boolean().optional(), enhancePosters: z.boolean().optional(), excludeSeederRange: z .tuple([z.number().min(0), z.number().min(0)]) .optional(), includeSeederRange: z .tuple([z.number().min(0), z.number().min(0)]) .optional(), requiredSeederRange: z .tuple([z.number().min(0), z.number().min(0)]) .optional(), seederRangeTypes: z.array(z.enum(['p2p', 'cached', 'uncached'])).optional(), excludeCached: z.boolean().optional(), excludeCachedFromAddons: z.array(z.string().min(1)).optional(), excludeCachedFromServices: z.array(z.string().min(1)).optional(), excludeCachedFromStreamTypes: z.array(StreamTypes).optional(), excludeCachedMode: z.enum(['or', 'and']).optional(), excludeUncached: z.boolean().optional(), excludeUncachedFromAddons: z.array(z.string().min(1)).optional(), excludeUncachedFromServices: z.array(z.string().min(1)).optional(), excludeUncachedFromStreamTypes: z.array(StreamTypes).optional(), excludeUncachedMode: z.enum(['or', 'and']).optional(), excludedStreamExpressions: z.array(z.string().min(1).max(1000)).optional(), requiredStreamExpressions: z.array(z.string().min(1).max(1000)).optional(), preferredStreamExpressions: z.array(z.string().min(1).max(1000)).optional(), groups: z .array( z.object({ addons: z.array(z.string().min(1)), condition: z.string().min(1).max(200), }) ) .optional(), sortCriteria: z.object({ // global must be defined. global: z.array(SortCriterion), // results must be from either a movie or series search, so we can safely apply different sort criteria. movies: z.array(SortCriterion).optional(), series: z.array(SortCriterion).optional(), anime: z.array(SortCriterion).optional(), // cached and uncached results are a sort criteria themselves, so this can only be applied when cache is high enough in the global // sort criteria, and we would have to split the results into two (cached and uncached) lists, and then apply both sort criteria below // and then merge the results. cached: z.array(SortCriterion).optional(), uncached: z.array(SortCriterion).optional(), cachedMovies: z.array(SortCriterion).optional(), uncachedMovies: z.array(SortCriterion).optional(), cachedSeries: z.array(SortCriterion).optional(), uncachedSeries: z.array(SortCriterion).optional(), cachedAnime: z.array(SortCriterion).optional(), uncachedAnime: z.array(SortCriterion).optional(), }), rpdbApiKey: z.string().optional(), formatter: Formatter, proxy: StreamProxyConfig.optional(), resultLimits: ResultLimitOptions.optional(), size: SizeFilterOptions.optional(), hideErrors: z.boolean().optional(), hideErrorsForResources: z.array(ResourceSchema).optional(), tmdbAccessToken: z.string().optional(), titleMatching: z .object({ mode: z.enum(['exact', 'contains']).optional(), matchYear: z.boolean().optional(), enabled: z.boolean().optional(), requestTypes: z.array(z.string()).optional(), addons: z.array(z.string()).optional(), }) .optional(), seasonEpisodeMatching: z .object({ enabled: z.boolean().optional(), requestTypes: z.array(z.string()).optional(), addons: z.array(z.string()).optional(), }) .optional(), deduplicator: DeduplicatorOptions.optional(), precacheNextEpisode: z.boolean().optional(), alwaysPrecache: z.boolean().optional(), services: ServiceList.optional(), presets: PresetList, catalogModifications: z.array(CatalogModification).optional(), externalDownloads: z.boolean().optional(), }); export type UserData = z.infer; export const TABLES = { USERS: ` uuid TEXT PRIMARY KEY, password_hash TEXT NOT NULL, config TEXT NOT NULL, config_salt TEXT NOT NULL, created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), updated_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP), accessed_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP) `, }; const strictManifestResourceSchema = z.object({ name: z.enum(constants.RESOURCES), types: z.array(z.string()), idPrefixes: z.array(z.string().min(1)).or(z.null()).optional(), }); export type StrictManifestResource = z.infer< typeof strictManifestResourceSchema >; const ManifestResourceSchema = z.union([ z.string(), strictManifestResourceSchema, ]); const ManifestExtraSchema = z.object({ name: z.string().min(1), isRequired: z.boolean().optional(), options: z.array(z.string().or(z.null())).or(z.null()).optional(), optionsLimit: z.number().min(1).optional(), }); const ManifestCatalogSchema = z.object({ type: z.string(), id: z.string().min(1), name: z.string().min(1), extra: z.array(ManifestExtraSchema).optional(), }); const AddonCatalogDefinitionSchema = z.object({ type: z.string(), id: z.string().min(1), name: z.string().min(1), }); export const ManifestSchema = z .object({ id: z.string().min(1), name: z.string(), description: z.string(), version: z.string(), types: z.array(z.string()), idPrefixes: z.array(z.string().min(1)).or(z.null()).optional(), resources: z.array(ManifestResourceSchema), catalogs: z.array(ManifestCatalogSchema), addonCatalogs: z.array(AddonCatalogDefinitionSchema).optional(), background: z.string().or(z.null()).optional(), logo: z.string().or(z.null()).optional(), contactEmail: z.string().or(z.null()).optional(), behaviorHints: z .object({ adult: z.boolean().optional(), p2p: z.boolean().optional(), configurable: z.boolean().optional(), configurationRequired: z.boolean().optional(), }) .optional(), // not part of the manifest scheme, but needed for stremio-addons.net stremioAddonsConfig: z .object({ issuer: z.string().min(1), signature: z.string().min(1), }) .optional(), }) .passthrough(); export type Manifest = z.infer; export const SubtitleSchema = z .object({ id: z.string().min(1), url: z.string().url(), lang: z.string().min(1), }) .passthrough(); export const SubtitleResponseSchema = z.object({ subtitles: z.array(SubtitleSchema), }); export type SubtitleResponse = z.infer; export type Subtitle = z.infer; export const StreamSchema = z .object({ url: z.string().url().or(z.null()).optional(), ytId: z.string().min(1).or(z.null()).optional(), infoHash: z.string().min(1).or(z.null()).optional(), fileIdx: z.number().or(z.null()).optional(), externalUrl: z.string().min(1).or(z.null()).optional(), name: z.string().min(1).or(z.null()).optional(), title: z.string().min(1).or(z.null()).optional(), description: z.string().min(1).or(z.null()).optional(), subtitles: z.array(SubtitleSchema).or(z.null()).optional(), sources: z.array(z.string().min(1)).or(z.null()).optional(), behaviorHints: z .object({ countryWhitelist: z.array(z.string().length(3)).or(z.null()).optional(), notWebReady: z.boolean().or(z.null()).optional(), bingeGroup: z.string().min(1).or(z.null()).optional(), proxyHeaders: z .object({ request: z.record(z.string().min(1), z.string().min(1)).optional(), response: z.record(z.string().min(1), z.string().min(1)).optional(), }) .optional(), videoHash: z.string().min(1).or(z.null()).optional(), videoSize: z.number().or(z.null()).optional(), filename: z.string().min(1).or(z.null()).optional(), }) .optional(), }) .passthrough(); export const StreamResponseSchema = z.object({ streams: z.array(StreamSchema), }); export type StreamResponse = z.infer; export type Stream = z.infer; const TrailerSchema = z .object({ source: z.string().min(1), type: z.enum(['Trailer', 'Clip']), }) .passthrough(); const MetaLinkSchema = z .object({ name: z.string().min(1), category: z.string().min(1), url: z.string().url().or(z.string().startsWith('stremio:///')), }) .passthrough(); const MetaVideoSchema = z .object({ id: z.string().min(1), title: z.string().or(z.null()).optional(), name: z.string().or(z.null()).optional(), released: z.string().datetime().or(z.null()).optional(), thumbnail: z.string().url().or(z.null()).optional(), streams: z.array(StreamSchema).or(z.null()).optional(), available: z.boolean().or(z.null()).optional(), episode: z.number().or(z.null()).optional(), season: z.number().or(z.null()).optional(), trailers: z.array(TrailerSchema).or(z.null()).optional(), overview: z.string().or(z.null()).optional(), }) .passthrough(); export const MetaPreviewSchema = z .object({ id: z.string().min(1), type: z.string().min(1), name: z.string().or(z.null()).optional(), poster: z.string().or(z.null()).optional(), posterShape: z .enum(['square', 'poster', 'landscape', 'regular']) .optional(), // discover sidebar //@deprecated use links instead genres: z.array(z.string()).or(z.null()).optional(), imdbRating: z.string().or(z.null()).or(z.number()).optional(), releaseInfo: z.string().or(z.number()).or(z.null()).optional(), //@deprecated director: z.array(z.string()).or(z.null()).optional(), //@deprecated cast: z.array(z.string()).or(z.null()).optional(), // background: z.string().min(1).optional(), // logo: z.string().min(1).optional(), description: z.string().or(z.null()).optional(), trailers: z.array(TrailerSchema).or(z.null()).optional(), links: z.array(MetaLinkSchema).or(z.null()).optional(), // released: z.string().datetime().optional(), }) .passthrough(); export const MetaSchema = MetaPreviewSchema.extend({ poster: z.string().or(z.null()).optional(), background: z.string().or(z.null()).optional(), logo: z.string().or(z.null()).optional(), videos: z.array(MetaVideoSchema).or(z.null()).optional(), runtime: z.coerce.string().or(z.null()).optional(), language: z.string().or(z.null()).optional(), country: z.string().or(z.null()).optional(), awards: z.string().or(z.null()).optional(), website: z.string().url().or(z.null()).optional(), behaviorHints: z .object({ defaultVideoId: z.string().or(z.null()).optional(), }) .optional(), }).passthrough(); export const MetaResponseSchema = z.object({ meta: MetaSchema, }); export const CatalogResponseSchema = z.object({ metas: z.array(MetaPreviewSchema), }); export type MetaResponse = z.infer; export type CatalogResponse = z.infer; export type Meta = z.infer; export type MetaPreview = z.infer; export const AddonCatalogSchema = z .object({ transportName: z.literal('http'), transportUrl: z.string().url(), manifest: ManifestSchema, }) .passthrough(); export const AddonCatalogResponseSchema = z.object({ addons: z.array(AddonCatalogSchema), }); export type AddonCatalogResponse = z.infer; export type AddonCatalog = z.infer; const ParsedFileSchema = z.object({ releaseGroup: z.string().optional(), resolution: z.string().optional(), quality: z.string().optional(), encode: z.string().optional(), audioChannels: z.array(z.string()), visualTags: z.array(z.string()), audioTags: z.array(z.string()), languages: z.array(z.string()), title: z.string().optional(), year: z.coerce.string().optional(), season: z.number().optional(), seasons: z.array(z.number()).optional(), episode: z.number().optional(), seasonEpisode: z.array(z.string()).optional(), }); export type ParsedFile = z.infer; export const ParsedStreamSchema = z.object({ id: z.string().min(1), proxied: z.boolean().optional(), addon: AddonSchema, parsedFile: ParsedFileSchema.optional(), message: z.string().max(1000).optional(), regexMatched: z .object({ name: z.string().optional(), pattern: z.string().min(1).optional(), index: z.number(), }) .optional(), keywordMatched: z.boolean().optional(), streamExpressionMatched: z.number().optional(), size: z.number().optional(), folderSize: z.number().optional(), type: StreamTypes, indexer: z.string().optional(), age: z.string().optional(), torrent: z .object({ infoHash: z.string().min(1).optional(), fileIdx: z.number().optional(), seeders: z.number().optional(), sources: z.array(z.string().min(1)).optional(), // array of tracker urls and DHT nodes }) .optional(), countryWhitelist: z.array(z.string().length(3)).optional(), notWebReady: z.boolean().optional(), bingeGroup: z.string().min(1).optional(), requestHeaders: z.record(z.string().min(1), z.string().min(1)).optional(), responseHeaders: z.record(z.string().min(1), z.string().min(1)).optional(), videoHash: z.string().min(1).optional(), subtitles: z.array(SubtitleSchema).optional(), filename: z.string().optional(), folderName: z.string().optional(), service: z .object({ id: z.enum(constants.SERVICES), cached: z.boolean(), }) .optional(), duration: z.number().optional(), library: z.boolean().optional(), url: z.string().url().optional(), ytId: z.string().min(1).optional(), externalUrl: z.string().min(1).optional(), error: z .object({ title: z.string().min(1), description: z.string().min(1), }) .optional(), originalName: z.string().optional(), originalDescription: z.string().optional(), }); export const ParsedStreams = z.array(ParsedStreamSchema); export type ParsedStream = z.infer; export type ParsedStreams = z.infer; export const AIOStream = StreamSchema.extend({ streamData: z.object({ error: z .object({ title: z.string().min(1), description: z.string().min(1), }) .optional(), proxied: z.boolean().optional(), addon: z.string().optional(), filename: z.string().optional(), folderName: z.string().optional(), service: z .object({ id: z.enum(constants.SERVICES), cached: z.boolean(), }) .optional(), parsedFile: ParsedFileSchema.optional(), message: z.string().max(1000).optional(), regexMatched: z .object({ name: z.string().optional(), pattern: z.string().min(1).optional(), index: z.number(), }) .optional(), keywordMatched: z.boolean().optional(), streamExpressionMatched: z.number().optional(), size: z.number().optional(), folderSize: z.number().optional(), type: StreamTypes.optional(), indexer: z.string().optional(), age: z.string().optional(), torrent: z .object({ infoHash: z.string().min(1).optional(), fileIdx: z.number().optional(), seeders: z.number().optional(), sources: z.array(z.string().min(1)).optional(), // array of tracker urls and DHT nodes }) .optional(), duration: z.number().optional(), library: z.boolean().optional(), }), }); export type AIOStream = z.infer; const AIOStreamResponseSchema = z.object({ streams: z.array(AIOStream), }); export type AIOStreamResponse = z.infer; const PresetMetadataSchema = z.object({ ID: z.string(), NAME: z.string(), DISABLED: z .object({ reason: z.string(), disabled: z.boolean(), }) .optional(), LOGO: z.string().optional(), DESCRIPTION: z.string(), URL: z.string(), TIMEOUT: z.number(), USER_AGENT: z.string(), SUPPORTED_SERVICES: z.array(z.string()), OPTIONS: z.array(OptionDefinition), SUPPORTED_STREAM_TYPES: z.array(StreamTypes), SUPPORTED_RESOURCES: z.array(ResourceSchema), }); const PresetMinimalMetadataSchema = z.object({ ID: z.string(), NAME: z.string(), LOGO: z.string().optional(), 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(), commit: z.string(), buildTime: z.string(), commitTime: z.string(), users: z.number().or(z.null()), settings: z.object({ baseUrl: z.string().url().optional(), addonName: z.string(), customHtml: z.string().optional(), protected: z.boolean(), regexFilterAccess: z.enum(['none', 'trusted', 'all']), tmdbApiAvailable: z.boolean(), forced: z.object({ proxy: z.object({ enabled: z.boolean().or(z.null()), id: z.string().or(z.null()), url: z.string().or(z.null()), publicIp: z.string().or(z.null()), credentials: z.string().or(z.null()), disableProxiedAddons: z.boolean(), proxiedServices: z.array(z.string()).or(z.null()), }), }), defaults: z.object({ proxy: z.object({ enabled: z.boolean().or(z.null()), id: z.string().or(z.null()), url: z.string().or(z.null()), publicIp: z.string().or(z.null()), credentials: z.string().or(z.null()), proxiedServices: z.array(z.string()).or(z.null()), }), timeout: z.number().or(z.null()), }), presets: z.array(PresetMinimalMetadataSchema), services: z.record( z.enum(constants.SERVICES), z.object({ id: z.enum(constants.SERVICES), name: z.string(), shortName: z.string(), knownNames: z.array(z.string()), signUpText: z.string(), credentials: z.array(OptionDefinition), }) ), }), }); export type StatusResponse = z.infer; export type PresetMetadata = z.infer; export type PresetMinimalMetadata = z.infer; export const RPDBIsValidResponse = z.object({ valid: z.boolean(), }); export type RPDBIsValidResponse = z.infer;