diff --git a/src/app/cron.ts b/src/app/cron.ts new file mode 100644 index 0000000..c2e704f --- /dev/null +++ b/src/app/cron.ts @@ -0,0 +1,26 @@ +import cron from 'node-cron'; +import type { Logger } from '../core/logging/logger'; +import type { AppConfig } from './config/AppConfig'; +import type { RecommendationPipeline } from './pipelines/RecommendationPipeline'; + +export function scheduleCron(config: AppConfig, pipeline: RecommendationPipeline, logger: Logger): void { + if (!config.CRON_ENABLED) return; + + cron.schedule(config.CRON_SCHEDULE, async () => { + logger.info('Cron: running recommendation pipeline'); + try { + const recommendations = await pipeline.runAsync({ + count: config.RECOMMEND_COUNT, + useLlm: config.LLM_ENABLED, + }); + if (config.RADARR_ENABLED) { + await pipeline.autoRequest(recommendations); + } + logger.info({ count: recommendations.length }, 'Cron: pipeline finished'); + } catch (err) { + logger.error({ err }, 'Cron: pipeline failed'); + } + }); + + logger.info({ schedule: config.CRON_SCHEDULE }, 'Cron scheduled'); +} diff --git a/src/app/index.ts b/src/app/index.ts new file mode 100644 index 0000000..19fc451 --- /dev/null +++ b/src/app/index.ts @@ -0,0 +1,86 @@ +import 'dotenv/config'; +import { loadAppConfig } from './config/AppConfig'; +import { createLogger } from '../core/logging/logger'; +import { LangfuseTracer } from '../core/langfuse/LangfuseTracer'; +import { LangfusePromptGateway } from '../core/langfuse/LangfusePromptGateway'; +import { PromptService } from '../core/langfuse/PromptService'; +import { LlmClient } from '../core/llm/LlmClient'; +import { buildServer } from '../core/server/buildServer'; +import { JellyfinClient } from './clients/jellyfin/JellyfinClient'; +import { TmdbClient } from './clients/tmdb/TmdbClient'; +import { RadarrClient } from './clients/radarr/RadarrClient'; +import { MovieRecommenderTool } from './tools/MovieRecommenderTool'; +import { MovieSearchTool } from './tools/MovieSearchTool'; +import { RecommendationPipeline } from './pipelines/RecommendationPipeline'; +import { HealthController } from './controllers/HealthController'; +import { RecommendationsController } from './controllers/RecommendationsController'; +import { RequestController } from './controllers/RequestController'; +import { WebhookController } from './controllers/WebhookController'; +import { scheduleCron } from './cron'; + +async function main(): Promise { + const config = loadAppConfig(); + const logger = createLogger('sugarr'); + + const langfuseOpts = { + publicKey: config.LANGFUSE_PUBLIC_KEY, + secretKey: config.LANGFUSE_SECRET_KEY, + baseUrl: config.LANGFUSE_BASE_URL, + }; + const tracer = new LangfuseTracer(langfuseOpts); + const promptGateway = new LangfusePromptGateway(langfuseOpts); + const promptService = new PromptService(promptGateway, config.LANGFUSE_PROMPT_CACHE_ENABLED); + const llmClient = new LlmClient({ baseUrl: config.LLM_BASE_URL, apiKey: config.LLM_API_KEY }); + + const jellyfin = new JellyfinClient(config.JELLYFIN_URL, config.JELLYFIN_API_KEY, config.JELLYFIN_USER_ID); + const tmdb = new TmdbClient(config.TMDB_API_KEY); + const radarr = config.RADARR_ENABLED + ? new RadarrClient(config.RADARR_URL, config.RADARR_API_KEY, config.RADARR_QUALITY_PROFILE_ID, config.RADARR_ROOT_FOLDER) + : undefined; + + const recommenderTool = new MovieRecommenderTool(promptService, llmClient); + const searchTool = new MovieSearchTool(promptService, llmClient); + + const pipeline = new RecommendationPipeline(tracer, logger, { + jellyfin, + tmdb, + radarr, + tool: recommenderTool, + searchTool, + minRatedMovies: config.MIN_RATED_MOVIES, + maxRetry: config.RECOMMEND_MAX_RETRY, + }); + + logger.info( + { + jellyfin: config.JELLYFIN_URL, + llmEnabled: config.LLM_ENABLED, + radarrEnabled: config.RADARR_ENABLED, + cronEnabled: config.CRON_ENABLED, + apiEnabled: config.API_ENABLED, + }, + 'sugarr starting' + ); + + scheduleCron(config, pipeline, logger); + + if (config.API_ENABLED) { + const app = buildServer({ + secretKey: config.SECRET_KEY, + controllers: [ + new HealthController(config), + new RecommendationsController(config, pipeline), + new RequestController(config, radarr), + new WebhookController(promptService), + ], + }); + await app.listen({ port: config.API_PORT, host: '0.0.0.0' }); + logger.info({ port: config.API_PORT }, 'HTTP API listening'); + } +} + +main().catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); +});