From 2790f4554193b3de24eaa8943388cab4cf1f6f24 Mon Sep 17 00:00:00 2001 From: Viren070 Date: Sat, 20 Sep 2025 20:10:38 +0100 Subject: [PATCH] feat: add knaben builtin addon --- packages/core/src/builtins/base/debrid.ts | 2 + packages/core/src/builtins/index.ts | 1 + packages/core/src/builtins/knaben/addon.ts | 131 +++++++++++++ packages/core/src/builtins/knaben/api.ts | 180 ++++++++++++++++++ packages/core/src/builtins/knaben/index.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/presets/knaben.ts | 68 +++++++ packages/core/src/presets/presetManager.ts | 6 +- packages/core/src/utils/env.ts | 13 ++ packages/core/src/utils/torrent.ts | 2 +- .../frontend/public/assets/knaben_logo.png | Bin 0 -> 5860 bytes packages/server/src/app.ts | 2 + packages/server/src/routes/builtins/index.ts | 1 + packages/server/src/routes/builtins/knaben.ts | 45 +++++ .../server/src/routes/builtins/torznab.ts | 1 - 15 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/builtins/knaben/addon.ts create mode 100644 packages/core/src/builtins/knaben/api.ts create mode 100644 packages/core/src/builtins/knaben/index.ts create mode 100644 packages/core/src/presets/knaben.ts create mode 100644 packages/frontend/public/assets/knaben_logo.png create mode 100644 packages/server/src/routes/builtins/knaben.ts diff --git a/packages/core/src/builtins/base/debrid.ts b/packages/core/src/builtins/base/debrid.ts index d4f800d6..df2bc425 100644 --- a/packages/core/src/builtins/base/debrid.ts +++ b/packages/core/src/builtins/base/debrid.ts @@ -35,6 +35,7 @@ export interface SearchMetadata extends TitleMetadata { imdbId?: string | null; tmdbId?: number | null; tvdbId?: number | null; + isAnime?: boolean; } export const BaseDebridConfigSchema = z.object({ @@ -314,6 +315,7 @@ export abstract class BaseDebridAddon { imdbId, tmdbId: metadata.tmdbId ?? null, tvdbId: metadata.tvdbId ?? null, + isAnime: animeEntry ? true : false, }; this.logger.debug( diff --git a/packages/core/src/builtins/index.ts b/packages/core/src/builtins/index.ts index 932620bb..6693a736 100644 --- a/packages/core/src/builtins/index.ts +++ b/packages/core/src/builtins/index.ts @@ -3,3 +3,4 @@ export * from './torbox-search/index.js'; export * from './torznab/index.js'; export * from './newznab/index.js'; export * from './prowlarr/index.js'; +export * from './knaben/index.js'; diff --git a/packages/core/src/builtins/knaben/addon.ts b/packages/core/src/builtins/knaben/addon.ts new file mode 100644 index 00000000..04256a53 --- /dev/null +++ b/packages/core/src/builtins/knaben/addon.ts @@ -0,0 +1,131 @@ +import { + BaseDebridAddon, + BaseDebridConfigSchema, + SearchMetadata, +} from '../base/debrid.js'; +import { z } from 'zod'; +import { + createLogger, + getTimeTakenSincePoint, + ParsedId, +} from '../../utils/index.js'; +import KnabenAPI, { KnabenCategory } from './api.js'; +import { NZB, UnprocessedTorrent } from '../../debrid/utils.js'; +import { extractTrackersFromMagnet } from '../utils/debrid.js'; + +const logger = createLogger('knaben'); + +export const KnabenAddonConfigSchema = BaseDebridConfigSchema; + +export type KnabenAddonConfig = z.infer; + +export class KnabenAddon extends BaseDebridAddon { + readonly id = 'knaben'; + readonly name = 'Knaben'; + readonly version = '1.0.0'; + readonly logger = logger; + readonly api: KnabenAPI; + + constructor(userData: KnabenAddonConfig, clientIp?: string) { + super(userData, KnabenAddonConfigSchema, clientIp); + this.api = new KnabenAPI(); + } + + protected async _searchNzbs( + parsedId: ParsedId, + metadata: SearchMetadata + ): Promise { + return []; + } + + protected async _searchTorrents( + parsedId: ParsedId, + metadata: SearchMetadata + ): Promise { + const queries = []; + let categories: number[] = []; + if (!metadata.primaryTitle) { + return []; + } + + if (parsedId.mediaType === 'movie') { + categories.push(KnabenCategory.Movies); + queries.push(`${metadata.primaryTitle} ${metadata.year}`); + } else { + categories.push(KnabenCategory.TV); + if (metadata.isAnime) { + categories.push(KnabenCategory.Anime); + } + if (metadata.absoluteEpisode) { + queries.push( + `${metadata.primaryTitle} ${metadata.absoluteEpisode.toString().padStart(2, '0')}` + ); + } + if (parsedId.season) { + queries.push( + `${metadata.primaryTitle} S${parsedId.season.toString().padStart(2, '0')}` + ); + } + if (parsedId.season && parsedId.episode) { + queries.push( + `${metadata.primaryTitle} S${parsedId.season.toString().padStart(2, '0')}E${parsedId.episode.toString().padStart(2, '0')}` + ); + } + } + + if (queries.length === 0) { + return []; + } + + logger.info(`Performing knaben search`, { queries, categories }); + + const searchPromises = queries.map(async (q) => { + const start = Date.now(); + const { hits } = await this.api.search({ + query: q, + categories, + size: 300, + }); + logger.info( + `Knaben search for ${q} took ${getTimeTakenSincePoint(start)}`, + { + results: hits.length, + } + ); + return hits; + }); + + const allResults = await Promise.all(searchPromises); + const hits = allResults.flat(); + + const seenTorrents = new Set(); + const torrents: UnprocessedTorrent[] = []; + for (const hit of hits) { + if (!hit.hash && !hit.link) { + logger.warn( + `Knaben search hit has no hash or download url: ${JSON.stringify(hit)}` + ); + continue; + } + if (seenTorrents.has(hit.hash ?? hit.link ?? '')) { + continue; + } + let sources: string[] = []; + if (hit.magnetUrl) { + sources = extractTrackersFromMagnet(hit.magnetUrl); + } + + torrents.push({ + hash: hit.hash ?? undefined, + downloadUrl: hit.link ?? undefined, + sources, + indexer: hit.tracker, + seeders: hit.seeders, + title: hit.title, + size: hit.bytes, + type: 'torrent', + }); + } + return torrents; + } +} diff --git a/packages/core/src/builtins/knaben/api.ts b/packages/core/src/builtins/knaben/api.ts new file mode 100644 index 00000000..81a9c335 --- /dev/null +++ b/packages/core/src/builtins/knaben/api.ts @@ -0,0 +1,180 @@ +import { Cache } from '../../utils/cache.js'; +import { Env } from '../../utils/env.js'; +import { formatZodError, makeRequest } from '../../utils/index.js'; +import { createLogger } from '../../utils/index.js'; + +import { z } from 'zod'; + +const logger = createLogger('knaben'); + +enum KnabenCategory { + TV = 2000000, + Movies = 3000000, + Anime = 6000000, +} + +const KnabenSearchHitSchema = z.looseObject({ + bytes: z.number(), + cachedOrigin: z.string(), + category: z.string(), + categoryId: z.array(z.number()), + date: z.iso.datetime({ offset: true }), + details: z.url(), + hash: z + .string() + .nullable() + .transform((val) => (val ? val.toLowerCase() : null)), + id: z.string(), + lastSeen: z.iso.datetime({ offset: true }), + magnetUrl: z.string().nullable(), + link: z.url().nullable(), + peers: z.number(), + seeders: z.number(), + score: z.number().nullable(), + title: z.string(), + tracker: z.string(), + trackerId: z.string(), + virusDetection: z.number().min(0).max(1), +}); + +type KnabenSearchHit = z.infer; + +const KnabenSearchResponse = z.object({ + hits: z.array(KnabenSearchHitSchema), + max_score: z.number().nullable(), + total: z.object({ + relation: z.enum(['eq', 'gte', 'lte']), + value: z.number(), + }), +}); + +type KnabenSearchResponse = z.infer; + +const KnabenSearchOptions = z.object({ + searchType: z + .union([ + z.literal('score'), + z.string().regex(/^(100|[1-9]?\d)%$/, { + message: 'Must be "score" or a percentage string like "80%"', + }), + ]) + .default('score') + .optional(), + searchField: z.keyof(KnabenSearchHitSchema).default('title').optional(), + query: z.string(), + orderBy: z.keyof(KnabenSearchHitSchema).optional(), + orderDirection: z.enum(['asc', 'desc']).default('desc').optional(), + categories: z.array(z.number()).optional(), + from: z.number().default(0).optional(), + size: z.number().min(1).max(300).default(150).optional(), + hideUnsafe: z.boolean().default(false).optional(), + hideXXX: z.boolean().default(true).optional(), + secondsSinceLastSeen: z.number().optional(), +}); + +type KnabenSearchOptions = z.infer; + +const KnabenSearchOptionsRequest = KnabenSearchOptions.transform((data) => ({ + search_type: data['searchType'], + search_field: data['searchField'], + query: data['query'], + order_by: data['orderBy'], + order_direction: data['orderDirection'], + categories: data['categories'], + from: data['from'], + size: data['size'], + hide_unsafe: data['hideUnsafe'], + hide_xxx: data['hideXXX'], + seconds_since_last_seen: data['secondsSinceLastSeen'], +})); + +const API_BASE_URL = 'https://api.knaben.org'; +const API_VERSION = '1'; + +class KnabenAPI { + private headers: Record; + + private readonly searchCache = Cache.getInstance( + 'knaben:search' + ); + + constructor() { + this.headers = { + 'Content-Type': 'application/json', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'application/json', + }; + } + + async search(options: KnabenSearchOptions): Promise { + const body = KnabenSearchOptionsRequest.parse(options); + + return this.searchCache.wrap( + () => + this.request('', { + schema: KnabenSearchResponse, + method: 'POST', + timeout: Env.BUILTIN_KNABEN_SEARCH_TIMEOUT, + body, + }), + `knaben:search:${JSON.stringify(options)}`, + Env.BUILTIN_KNABEN_SEARCH_CACHE_TTL + ); + } + + private async request( + endpoint: string, + options: { + schema: z.ZodSchema; + body?: unknown; + method?: string; + timeout?: number; + } + ): Promise { + const { schema, body, method = 'GET' } = options; + let path = `/v${API_VERSION}`; + if (endpoint) { + path += `/${endpoint.startsWith('/') ? endpoint.slice(1) : endpoint}`; + } + const url = new URL(path, API_BASE_URL); + + logger.debug(`Making ${method} request to ${path}`); + + try { + const response = await makeRequest(url.toString(), { + method, + headers: this.headers, + body: body ? JSON.stringify(body) : undefined, + timeout: options.timeout ?? Env.MAX_TIMEOUT, + }); + + const data = (await response.json()) as unknown; + + if (!response.ok) { + throw new Error( + `Knaben API error (${response.status}): ${response.statusText}` + ); + } + + try { + return schema.parse(data); + } catch (error) { + throw new Error( + `Failed to parse Knaben API response: ${formatZodError(error as z.ZodError)}` + ); + } + } catch (error) { + logger.error( + `Request to ${path} failed: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + throw error instanceof Error + ? error + : new Error('Unknown error occurred'); + } + } +} + +export { KnabenCategory }; +export type { KnabenSearchOptions, KnabenSearchResponse, KnabenSearchHit }; +export default KnabenAPI; diff --git a/packages/core/src/builtins/knaben/index.ts b/packages/core/src/builtins/knaben/index.ts new file mode 100644 index 00000000..3ee341c5 --- /dev/null +++ b/packages/core/src/builtins/knaben/index.ts @@ -0,0 +1 @@ +export * from './addon.js'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index caa5e7ce..7e7115fd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,5 +14,6 @@ export { TorznabAddon, NewznabAddon, ProwlarrAddon, + KnabenAddon, } from './builtins/index.js'; export { PresetManager } from './presets/index.js'; diff --git a/packages/core/src/presets/knaben.ts b/packages/core/src/presets/knaben.ts new file mode 100644 index 00000000..bbb7ef84 --- /dev/null +++ b/packages/core/src/presets/knaben.ts @@ -0,0 +1,68 @@ +import { Option, UserData } from '../db/index.js'; +import { Env, constants } from '../utils/index.js'; +import { StremThruPreset } from './stremthru.js'; +import { TorznabPreset } from './torznab.js'; + +export class KnabenPreset extends TorznabPreset { + static override get METADATA() { + const supportedResources = [constants.STREAM_RESOURCE]; + const options: Option[] = [ + { + id: 'name', + name: 'Name', + description: 'What to call this addon', + type: 'string', + required: true, + default: 'Knaben', + }, + { + id: 'timeout', + name: 'Timeout', + description: 'The timeout for this addon', + type: 'number', + required: true, + default: Env.BUILTIN_KNABEN_TIMEOUT || Env.DEFAULT_TIMEOUT, + }, + { + id: 'services', + name: 'Services', + description: + 'Optionally override the services that are used. If not specified, then the services that are enabled and supported will be used.', + type: 'multi-select', + required: false, + showInNoobMode: false, + options: StremThruPreset.supportedServices.map((service) => ({ + value: service, + label: constants.SERVICE_DETAILS[service].name, + })), + default: undefined, + emptyIsUndefined: true, + }, + ]; + + return { + ID: 'knaben', + NAME: 'Knaben', + LOGO: '/assets/knaben_logo.png', + URL: `${Env.INTERNAL_URL}/builtins/knaben`, + TIMEOUT: Env.BUILTIN_KNABEN_TIMEOUT || Env.DEFAULT_TIMEOUT, + USER_AGENT: Env.DEFAULT_USER_AGENT, + SUPPORTED_SERVICES: StremThruPreset.supportedServices, + DESCRIPTION: 'Directly search Knaben for results with your services.', + OPTIONS: options, + SUPPORTED_STREAM_TYPES: [constants.DEBRID_STREAM_TYPE], + SUPPORTED_RESOURCES: supportedResources, + BUILTIN: true, + }; + } + + protected static override generateManifestUrl( + userData: UserData, + services: constants.ServiceId[], + options: Record + ): string { + return `${Env.INTERNAL_URL}/builtins/knaben/${this.base64EncodeJSON( + this.getBaseConfig(userData, services) + )}/manifest.json`; + } +} diff --git a/packages/core/src/presets/presetManager.ts b/packages/core/src/presets/presetManager.ts index da0aff25..348fe69c 100644 --- a/packages/core/src/presets/presetManager.ts +++ b/packages/core/src/presets/presetManager.ts @@ -61,6 +61,7 @@ import { NewznabPreset } from './newznab.js'; import { ProwlarrPreset } from './prowlarr.js'; import { JackettPreset } from './jackett.js'; import { NZBHydraPreset } from './nzbhydra.js'; +import { KnabenPreset } from './knaben.js'; let PRESET_LIST: string[] = [ 'custom', @@ -72,8 +73,9 @@ let PRESET_LIST: string[] = [ 'mediafusion', 'stremthruTorz', 'stremthruStore', - 'animetosho', 'zilean', + 'knaben', + 'animetosho', 'prowlarr', 'jackett', 'nzbhydra', @@ -264,6 +266,8 @@ export class PresetManager { return JackettPreset; case 'nzbhydra': return NZBHydraPreset; + case 'knaben': + return KnabenPreset; default: throw new Error(`Preset ${id} not found`); } diff --git a/packages/core/src/utils/env.ts b/packages/core/src/utils/env.ts index fc2c7ba4..8a76b51f 100644 --- a/packages/core/src/utils/env.ts +++ b/packages/core/src/utils/env.ts @@ -1726,6 +1726,19 @@ export const Env = cleanEnv(process.env, { desc: 'Builtin Prowlarr Indexers cache TTL', }), + BUILTIN_KNABEN_TIMEOUT: num({ + default: undefined, + desc: 'Builtin Knaben timeout', + }), + BUILTIN_KNABEN_SEARCH_TIMEOUT: num({ + default: 30000, // 30 seconds + desc: 'Builtin Knaben Search timeout', + }), + BUILTIN_KNABEN_SEARCH_CACHE_TTL: num({ + default: 7 * 24 * 60 * 60, // 7 days + desc: 'Builtin Knaben Search cache TTL', + }), + // Rate limiting settings DISABLE_RATE_LIMITS: bool({ default: false, diff --git a/packages/core/src/utils/torrent.ts b/packages/core/src/utils/torrent.ts index 44141703..4d16c915 100644 --- a/packages/core/src/utils/torrent.ts +++ b/packages/core/src/utils/torrent.ts @@ -87,7 +87,7 @@ export class TorrentClient { let metadata: TorrentMetadata; // Handle redirects - if (response.status === 302 || response.status === 301) { + if (response.status >= 300 && response.status < 400) { const redirectUrl = response.headers.get('Location'); if (!redirectUrl) { throw new Error('Redirect location not found'); diff --git a/packages/frontend/public/assets/knaben_logo.png b/packages/frontend/public/assets/knaben_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..995837435d9a5e2f695e7261762b6a21ad5786bd GIT binary patch literal 5860 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 literal 0 HcmV?d00001 diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 98a89e2d..6913c5b2 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -27,6 +27,7 @@ import { torznab, newznab, prowlarr, + knaben, } from './routes/builtins/index.js'; import { ipMiddleware, @@ -130,6 +131,7 @@ builtinsRouter.use('/torbox-search', torboxSearch); builtinsRouter.use('/torznab', torznab); builtinsRouter.use('/newznab', newznab); builtinsRouter.use('/prowlarr', prowlarr); +builtinsRouter.use('/knaben', knaben); app.use('/builtins', builtinsRouter); app.get( diff --git a/packages/server/src/routes/builtins/index.ts b/packages/server/src/routes/builtins/index.ts index 6ca9a73a..43de07de 100644 --- a/packages/server/src/routes/builtins/index.ts +++ b/packages/server/src/routes/builtins/index.ts @@ -3,3 +3,4 @@ export { default as torboxSearch } from './torbox-search.js'; export { default as torznab } from './torznab.js'; export { default as newznab } from './newznab.js'; export { default as prowlarr } from './prowlarr.js'; +export { default as knaben } from './knaben.js'; diff --git a/packages/server/src/routes/builtins/knaben.ts b/packages/server/src/routes/builtins/knaben.ts new file mode 100644 index 00000000..49d6ca2a --- /dev/null +++ b/packages/server/src/routes/builtins/knaben.ts @@ -0,0 +1,45 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { AIOStreams, AIOStreamResponse, KnabenAddon } from '@aiostreams/core'; +import { createLogger } from '@aiostreams/core'; +const router: Router = Router(); + +const logger = createLogger('server'); + +router.get( + '/:encodedConfig/manifest.json', + async (req: Request, res: Response, next: NextFunction) => { + const { encodedConfig } = req.params; + const config = encodedConfig + ? JSON.parse(Buffer.from(encodedConfig, 'base64').toString('utf-8')) + : undefined; + + try { + const manifest = new KnabenAddon(config, req.userIp).getManifest(); + res.json(manifest); + } catch (error) { + next(error); + } + } +); + +router.get( + '/:encodedConfig/stream/:type/:id.json', + async (req: Request, res: Response, next: NextFunction) => { + const { encodedConfig, type, id } = req.params; + const config = JSON.parse( + Buffer.from(encodedConfig, 'base64').toString('utf-8') + ); + + try { + const addon = new KnabenAddon(config, req.userIp); + const streams = await addon.getStreams(type, id); + res.json({ + streams: streams, + }); + } catch (error) { + next(error); + } + } +); + +export default router; diff --git a/packages/server/src/routes/builtins/torznab.ts b/packages/server/src/routes/builtins/torznab.ts index 77132646..a3d707ed 100644 --- a/packages/server/src/routes/builtins/torznab.ts +++ b/packages/server/src/routes/builtins/torznab.ts @@ -1,6 +1,5 @@ import { Router, Request, Response, NextFunction } from 'express'; import { AIOStreams, AIOStreamResponse, TorznabAddon } from '@aiostreams/core'; -import { stremioStreamRateLimiter } from '../../middlewares/ratelimit'; import { createLogger } from '@aiostreams/core'; const router: Router = Router();