diff --git a/packages/core/src/builtins/prowlarr/addon.ts b/packages/core/src/builtins/prowlarr/addon.ts index bca0199f..5f2937ac 100644 --- a/packages/core/src/builtins/prowlarr/addon.ts +++ b/packages/core/src/builtins/prowlarr/addon.ts @@ -20,12 +20,14 @@ import { validateInfoHash, } from '../utils/debrid.js'; import { createQueryLimit, useAllTitles } from '../utils/general.js'; +import { createHash } from 'crypto'; export const ProwlarrAddonConfigSchema = BaseDebridConfigSchema.extend({ url: z.string(), apiKey: z.string(), indexers: z.array(z.string()), tags: z.array(z.string()), + sources: z.array(z.string()).optional(), }); export type ProwlarrAddonConfig = z.infer; @@ -44,6 +46,7 @@ export class ProwlarrAddon extends BaseDebridAddon { private readonly preconfiguredInstance: boolean; private readonly indexers: string[] = []; private readonly tags: string[] = []; + private readonly sources: string[] = []; constructor(config: ProwlarrAddonConfig, clientIp?: string) { super(config, ProwlarrAddonConfigSchema, clientIp); @@ -52,6 +55,7 @@ export class ProwlarrAddon extends BaseDebridAddon { Env.BUILTIN_PROWLARR_API_KEY === config.apiKey; this.indexers = config.indexers.map((x) => x.toLowerCase()); this.tags = config.tags.map((x) => x.toLowerCase()); + this.sources = (config.sources ?? []).map((x) => x.toLowerCase()); this.api = new ProwlarrApi({ baseUrl: config.url, apiKey: config.apiKey, @@ -79,13 +83,13 @@ export class ProwlarrAddon extends BaseDebridAddon { ); return false; } - if (indexer.protocol !== 'torrent') { - filterReasons.set( - 'not torrent protocol', - (filterReasons.get('not torrent protocol') ?? 0) + 1 - ); - return false; - } + // if (indexer.protocol !== 'torrent') { + // filterReasons.set( + // 'not torrent protocol', + // (filterReasons.get('not torrent protocol') ?? 0) + 1 + // ); + // return false; + // } if (Env.BUILTIN_PROWLARR_INDEXERS?.length) { if ( ![ @@ -119,13 +123,15 @@ export class ProwlarrAddon extends BaseDebridAddon { } } - protected async _searchTorrents( - parsedId: ParsedId, - metadata: SearchMetadata - ): Promise { - const queryLimit = createQueryLimit(); + /** + * Get indexers filtered by protocol (torrent or usenet) + */ + private async getIndexersByProtocol( + protocol: 'torrent' | 'usenet' + ): Promise { let availableIndexers: ProwlarrApiIndexer[] = []; let chosenTags: number[] = []; + if (this.preconfiguredInstance && ProwlarrAddon.preconfiguredIndexers) { availableIndexers = ProwlarrAddon.preconfiguredIndexers; } else { @@ -154,7 +160,7 @@ export class ProwlarrAddon extends BaseDebridAddon { const chosenIndexers = availableIndexers.filter( (indexer) => indexer.enable && - indexer.protocol === 'torrent' && + indexer.protocol === protocol && ((!this.indexers.length && !chosenTags.length) || (chosenTags.length && indexer.tags.some((tag) => chosenTags.includes(tag))) || @@ -165,9 +171,36 @@ export class ProwlarrAddon extends BaseDebridAddon { ); this.logger.info( - `Chosen indexers: ${chosenIndexers.map((indexer) => indexer.name).join(', ')}` + `Chosen ${protocol} indexers: ${chosenIndexers.map((indexer) => indexer.name).join(', ')}` ); + return chosenIndexers; + } + + /** + * Common search method that performs the actual Prowlarr API search + * @param protocol - The protocol type ('torrent' or 'usenet') + * @param parsedId - The parsed content ID + * @param metadata - Search metadata + * @returns Array of search results from Prowlarr + */ + private async performSearch( + protocol: 'torrent' | 'usenet', + parsedId: ParsedId, + metadata: SearchMetadata + ): Promise { + if (this.sources.length > 0 && !this.sources.includes(protocol)) { + return []; + } + + const queryLimit = createQueryLimit(); + const chosenIndexers = await this.getIndexersByProtocol(protocol); + + if (chosenIndexers.length === 0) { + this.logger.warn(`No ${protocol} indexers available`); + return []; + } + const queries = this.buildQueries(parsedId, metadata, { useAllTitles: useAllTitles(this.userData.url), }); @@ -184,7 +217,7 @@ export class ProwlarrAddon extends BaseDebridAddon { type: 'search', }); this.logger.info( - `Prowlarr search for ${q} took ${getTimeTakenSincePoint(start)}`, + `Prowlarr ${protocol} search for ${q} took ${getTimeTakenSincePoint(start)}`, { results: data.length, } @@ -193,7 +226,15 @@ export class ProwlarrAddon extends BaseDebridAddon { }) ); const allResults = await Promise.all(searchPromises); - const results = allResults.flat(); + return allResults.flat(); + } + + protected async _searchTorrents( + parsedId: ParsedId, + metadata: SearchMetadata + ): Promise { + const results = await this.performSearch('torrent', parsedId, metadata); + if (results.length === 0) return []; const seenTorrents = new Set(); const torrents: UnprocessedTorrent[] = []; @@ -231,6 +272,31 @@ export class ProwlarrAddon extends BaseDebridAddon { parsedId: ParsedId, metadata: SearchMetadata ): Promise { - return []; + const results = await this.performSearch('usenet', parsedId, metadata); + if (results.length === 0) return []; + + const seenNzbs = new Set(); + const nzbs: NZB[] = []; + + for (const result of results) { + const nzbUrl = result.downloadUrl ?? result.guid; + if (!nzbUrl) continue; + if (seenNzbs.has(nzbUrl)) continue; + seenNzbs.add(nzbUrl); + + const hash = createHash('md5').update(nzbUrl).digest('hex'); + const ageStr = result.age ? `${Math.ceil(result.age)}d` : '0d'; + + nzbs.push({ + hash, + nzb: nzbUrl, + age: ageStr, + title: result.title, + size: result.size, + indexer: result.indexer, + type: 'usenet', + }); + } + return nzbs; } } diff --git a/packages/core/src/presets/prowlarr.ts b/packages/core/src/presets/prowlarr.ts index 7de36ca3..81689b8e 100644 --- a/packages/core/src/presets/prowlarr.ts +++ b/packages/core/src/presets/prowlarr.ts @@ -97,6 +97,25 @@ export class ProwlarrPreset extends BuiltinAddonPreset { default: '', } as const, ]), + { + id: 'sources', + name: 'Sources', + description: + 'The sources to use when fetching from Prowlarr. If not specified, both torrent and usenet indexers will be used, if available.', + type: 'multi-select', + required: false, + showInSimpleMode: false, + options: [ + { + label: 'Torrent', + value: 'torrent', + }, + { + label: 'Usenet', + value: 'usenet', + }, + ], + }, { id: 'tags', name: 'Tags', @@ -165,10 +184,17 @@ export class ProwlarrPreset extends BuiltinAddonPreset { URL: `${Env.INTERNAL_URL}/builtins/prowlarr`, TIMEOUT: Env.DEFAULT_TIMEOUT, USER_AGENT: Env.DEFAULT_USER_AGENT, - SUPPORTED_SERVICES: StremThruPreset.supportedServices, + SUPPORTED_SERVICES: [ + ...StremThruPreset.supportedServices, + constants.NZBDAV_SERVICE, + constants.ALTMOUNT_SERVICE, + ], DESCRIPTION: 'An addon to get debrid results from a Prowlarr instance.', OPTIONS: options, - SUPPORTED_STREAM_TYPES: [constants.DEBRID_STREAM_TYPE], + SUPPORTED_STREAM_TYPES: [ + constants.DEBRID_STREAM_TYPE, + constants.USENET_STREAM_TYPE, + ], SUPPORTED_RESOURCES: supportedResources, BUILTIN: true, }; @@ -265,6 +291,7 @@ export class ProwlarrPreset extends BuiltinAddonPreset { url: prowlarrUrl, apiKey: prowlarrApiKey, indexers: indexers || [], + sources: options.sources || [], tags: typeof options.tags === 'string' ? options.tags.split(',') : [], };