feat: add ai search addon to marketplace

This commit is contained in:
Viren070
2025-07-13 18:54:33 +01:00
parent ac4d209879
commit c9c1babc2a
5 changed files with 309 additions and 0 deletions
+3
View File
@@ -589,4 +589,7 @@ SUBSOURCE_URL=https://subsource.strem.top
OPENSUBTITLES_V3_PLUS_URL=https://opensubtitles.stremio.homes
# DEFAULT_OPENSUBTITLES_V3_PLUS_TIMEOUT=
# ---------------- AI Search ----------------------------
AI_SEARCH_URL=https://stremio.itcon.au/aisearch
# ==============================================================================
+276
View File
@@ -0,0 +1,276 @@
import { Addon, Option, UserData } from '../db';
import { Preset, baseOptions } from './preset';
import { Cache, constants, Env, makeRequest } from '../utils';
import { z } from 'zod';
const configCache = Cache.getInstance<string, string>('ai-search-config');
export class AISearchPreset extends Preset {
private static languages = [
{ label: 'Afrikaans (South Africa)', value: 'af-ZA' },
{ label: 'Arabic (UAE)', value: 'ar-AE' },
{ label: 'Arabic (Saudi Arabia)', value: 'ar-SA' },
{ label: 'Belarusian (Belarus)', value: 'be-BY' },
{ label: 'Bulgarian (Bulgaria)', value: 'bg-BG' },
{ label: 'Bengali (Bangladesh)', value: 'bn-BD' },
{ label: 'Catalan (Spain)', value: 'ca-ES' },
{ label: 'Chamorro (Guam)', value: 'ch-GU' },
{ label: 'Czech (Czech Republic)', value: 'cs-CZ' },
{ label: 'Welsh (United Kingdom)', value: 'cy-GB' },
{ label: 'Danish (Denmark)', value: 'da-DK' },
{ label: 'German (Austria)', value: 'de-AT' },
{ label: 'German (Switzerland)', value: 'de-CH' },
{ label: 'German (Germany)', value: 'de-DE' },
{ label: 'Greek (Greece)', value: 'el-GR' },
{ label: 'English (Australia)', value: 'en-AU' },
{ label: 'English (Canada)', value: 'en-CA' },
{ label: 'English (United Kingdom)', value: 'en-GB' },
{ label: 'English (Ireland)', value: 'en-IE' },
{ label: 'English (New Zealand)', value: 'en-NZ' },
{ label: 'English (United States)', value: 'en-US' },
{ label: 'Esperanto', value: 'eo-EO' },
{ label: 'Spanish (Spain)', value: 'es-ES' },
{ label: 'Spanish (Mexico)', value: 'es-MX' },
{ label: 'Estonian (Estonia)', value: 'et-EE' },
{ label: 'Basque (Spain)', value: 'eu-ES' },
{ label: 'Persian (Iran)', value: 'fa-IR' },
{ label: 'Finnish (Finland)', value: 'fi-FI' },
{ label: 'French (Canada)', value: 'fr-CA' },
{ label: 'French (France)', value: 'fr-FR' },
{ label: 'Irish (Ireland)', value: 'ga-IE' },
{ label: 'Galician (Spain)', value: 'gl-ES' },
{ label: 'Hebrew (Israel)', value: 'he-IL' },
{ label: 'Hindi (India)', value: 'hi-IN' },
{ label: 'Croatian (Croatia)', value: 'hr-HR' },
{ label: 'Hungarian (Hungary)', value: 'hu-HU' },
{ label: 'Indonesian (Indonesia)', value: 'id-ID' },
{ label: 'Icelandic (Iceland)', value: 'is-IS' },
{ label: 'Italian (Italy)', value: 'it-IT' },
{ label: 'Japanese (Japan)', value: 'ja-JP' },
{ label: 'Georgian (Georgia)', value: 'ka-GE' },
{ label: 'Kazakh (Kazakhstan)', value: 'kk-KZ' },
{ label: 'Kannada (India)', value: 'kn-IN' },
{ label: 'Korean (South Korea)', value: 'ko-KR' },
{ label: 'Kyrgyz (Kyrgyzstan)', value: 'ky-KG' },
{ label: 'Lithuanian (Lithuania)', value: 'lt-LT' },
{ label: 'Latvian (Latvia)', value: 'lv-LV' },
{ label: 'Malayalam (India)', value: 'ml-IN' },
{ label: 'Marathi (India)', value: 'mr-IN' },
{ label: 'Malay (Malaysia)', value: 'ms-MY' },
{ label: 'Norwegian Bokmål (Norway)', value: 'nb-NO' },
{ label: 'Dutch (Belgium)', value: 'nl-BE' },
{ label: 'Dutch (Netherlands)', value: 'nl-NL' },
{ label: 'Norwegian (Norway)', value: 'no-NO' },
{ label: 'Punjabi (India)', value: 'pa-IN' },
{ label: 'Polish (Poland)', value: 'pl-PL' },
{ label: 'Portuguese (Brazil)', value: 'pt-BR' },
{ label: 'Portuguese (Portugal)', value: 'pt-PT' },
{ label: 'Romanian (Romania)', value: 'ro-RO' },
{ label: 'Russian (Russia)', value: 'ru-RU' },
{ label: 'Sinhala (Sri Lanka)', value: 'si-LK' },
{ label: 'Slovak (Slovakia)', value: 'sk-SK' },
{ label: 'Slovenian (Slovenia)', value: 'sl-SI' },
{ label: 'Albanian (Albania)', value: 'sq-AL' },
{ label: 'Serbian (Serbia)', value: 'sr-RS' },
{ label: 'Swedish (Sweden)', value: 'sv-SE' },
{ label: 'Tamil (India)', value: 'ta-IN' },
{ label: 'Telugu (India)', value: 'te-IN' },
{ label: 'Thai (Thailand)', value: 'th-TH' },
{ label: 'Tagalog (Philippines)', value: 'tl-PH' },
{ label: 'Turkish (Turkey)', value: 'tr-TR' },
{ label: 'Ukrainian (Ukraine)', value: 'uk-UA' },
{ label: 'Vietnamese (Vietnam)', value: 'vi-VN' },
{ label: 'Chinese (China)', value: 'zh-CN' },
{ label: 'Chinese (Hong Kong)', value: 'zh-HK' },
{ label: 'Chinese (Taiwan)', value: 'zh-TW' },
{ label: 'Zulu (South Africa)', value: 'zu-ZA' },
];
static override get METADATA() {
const supportedResources = [
constants.CATALOG_RESOURCE,
constants.META_RESOURCE,
];
const options: Option[] = [
...baseOptions(
'AI Search',
supportedResources,
Env.DEFAULT_AI_SEARCH_TIMEOUT
),
{
id: 'limitedConfig',
name: 'Limited Configuration',
description:
'To use Trakt-based recommendations, manually configure the AI Search addon and paste its Manifest/Installation URL above. The options below will not enable this feature.',
type: 'alert',
intent: 'warning-basic',
},
{
id: 'geminiApiKey',
name: 'Gemini API Key',
description:
'Get a free key from [Google AI Studio](https://makersuite.google.com/app/apikey)',
type: 'password',
required: true,
constraints: {
min: 10,
},
},
{
id: 'tmdbApiKey',
name: 'TMDB API Key',
description:
'Get a free key from [TMDB](https://www.themoviedb.org/settings/api)',
type: 'password',
required: true,
constraints: {
min: 10,
},
},
{
id: 'advancedSettingsNote',
type: 'alert',
name: 'Advanced Settings',
description: 'The below settings are for advanced users only.',
},
{
id: 'AiResponseCaching',
name: 'AI Response Caching',
description: 'Enable AI response caching',
type: 'boolean',
default: true,
},
{
id: 'rpdbApiKey',
name: 'RPDB API Key (Optional)',
description: 'Optionally provide an RPDB API Key to use for posters',
type: 'password',
required: false,
},
{
id: 'language',
name: 'Language',
description: 'The language to use for AI Search',
type: 'select',
options: this.languages,
default: 'en-US',
},
{
id: 'model',
name: 'Gemini Model Name',
description:
'The Gemini model to use for AI Search. See available models at the [documentation](https://ai.google.dev/gemini-api/docs/models/gemini)',
type: 'string',
default: 'gemini-2.0-flash-lite',
},
{
id: 'numberOfRecommendations',
name: 'Number of Recommendations',
description:
'The number of recommendations to return. Higher values may increase the response time.',
type: 'number',
default: 20,
constraints: {
min: 1,
max: 30,
},
},
];
return {
ID: 'ai-search',
NAME: 'AI Search',
LOGO: `https://raw.githubusercontent.com/itcon-pty-au/stremio-ai-search/refs/heads/main/public/logo.png`,
URL: Env.AI_SEARCH_URL,
TIMEOUT: Env.DEFAULT_AI_SEARCH_TIMEOUT || Env.DEFAULT_TIMEOUT,
USER_AGENT: Env.DEFAULT_AI_SEARCH_USER_AGENT || Env.DEFAULT_USER_AGENT,
SUPPORTED_SERVICES: [],
DESCRIPTION:
'AI-powered movie and series recommendations. Powered by Gemini.',
OPTIONS: options,
SUPPORTED_STREAM_TYPES: [],
SUPPORTED_RESOURCES: supportedResources,
};
}
static async generateAddons(
userData: UserData,
options: Record<string, any>
): Promise<Addon[]> {
return [await this.generateAddon(userData, options)];
}
private static async generateAddon(
userData: UserData,
options: Record<string, any>
): Promise<Addon> {
return {
name: options.name || this.METADATA.NAME,
manifestUrl: await this.generateManifestUrl(options),
enabled: true,
library: false,
resources: options.resources || this.METADATA.SUPPORTED_RESOURCES,
timeout: options.timeout || this.METADATA.TIMEOUT,
presetType: this.METADATA.ID,
presetInstanceId: '',
headers: {
'User-Agent': this.METADATA.USER_AGENT,
},
};
}
private static async generateManifestUrl(
options: Record<string, any>
): Promise<string> {
let url = (options.url || this.METADATA.URL).replace(/\/$/, '');
if (url.endsWith('/manifest.json')) {
return url;
}
let config = {
GeminiApiKey: options.geminiApiKey,
TmdbApiKey: options.tmdbApiKey,
RpdbApiKey: options.rpdbApiKey,
RpdbPosterType: options.rpdbApiKey ? 'poster-default' : undefined,
EnableRpdb: options.rpdbApiKey ? true : false,
EnableAiCache: options.AiResponseCaching,
TmdbLanguage: options.language,
GeminiModel: options.model,
};
// request to /aisearch/encrypt with cache and config as body
const cacheKey = `${JSON.stringify(config)}`;
let configId: string | undefined = configCache.get(cacheKey);
if (configId) {
console.log('CACHE HIT');
return `${url}/aisearch/${configId}/manifest.json`;
}
let response;
try {
response = await makeRequest(`${url}/aisearch/encrypt`, {
timeout: this.METADATA.TIMEOUT,
method: 'POST',
headers: {
'User-Agent': this.METADATA.USER_AGENT,
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
});
if (!response.ok) {
throw new Error(`${response.status} - ${response.statusText}`);
}
const data = await response.json();
const schema = z.object({
encryptedConfig: z.string(),
});
const result = schema.parse(data);
configId = result.encryptedConfig;
configCache.set(cacheKey, configId, 365 * 24 * 60 * 60);
return `${url}/aisearch/${configId}/manifest.json`;
} catch (error) {
throw new Error(
`Failed to generate manifest URL for AI Search: ${error}`
);
}
}
}
@@ -40,6 +40,7 @@ import { ArgentinaTVPreset } from './argentinaTv';
import { OpenSubtitlesV3PlusPreset } from './opensubtitles-v3-plus';
import { SubSourcePreset } from './subsource';
import { SubDLPreset } from './subdl';
import { AISearchPreset } from './aiSearch';
const PRESET_LIST: string[] = [
'custom',
@@ -65,6 +66,7 @@ const PRESET_LIST: string[] = [
'argentina-tv',
'debridio-tv',
'debridio-watchtower',
'ai-search',
'tmdb-addon',
'debridio-tmdb',
'debridio-tvdb',
@@ -186,6 +188,8 @@ export class PresetManager {
return SubSourcePreset;
case 'subdl':
return SubDLPreset;
case 'ai-search':
return AISearchPreset;
default:
throw new Error(`Preset ${id} not found`);
}
+13
View File
@@ -1212,6 +1212,19 @@ export const Env = cleanEnv(process.env, {
desc: 'Default OpenSubtitles V3 Plus user agent',
}),
AI_SEARCH_URL: url({
default: 'https://stremio.itcon.au',
desc: 'AI Search URL',
}),
DEFAULT_AI_SEARCH_TIMEOUT: num({
default: undefined,
desc: 'Default AI Search timeout',
}),
DEFAULT_AI_SEARCH_USER_AGENT: userAgent({
default: undefined,
desc: 'Default AI Search user agent',
}),
// Rate limiting settings
DISABLE_RATE_LIMITS: bool({
default: false,
+13
View File
@@ -1155,6 +1155,19 @@ const logStartupInfo = () => {
}
});
// AI Search
logKeyValue('AI Search:', Env.AI_SEARCH_URL);
if (Env.DEFAULT_AI_SEARCH_TIMEOUT) {
logKeyValue(
' Timeout:',
formatMilliseconds(Env.DEFAULT_AI_SEARCH_TIMEOUT),
' '
);
}
if (Env.DEFAULT_AI_SEARCH_USER_AGENT) {
logKeyValue(' User Agent:', Env.DEFAULT_AI_SEARCH_USER_AGENT, ' ');
}
// Additional Features
const features: string[] = [];
if (Env.TMDB_ACCESS_TOKEN) features.push('TMDB Integration');