diff --git a/src/app/config/AppConfig.ts b/src/app/config/AppConfig.ts new file mode 100644 index 0000000..56b18ab --- /dev/null +++ b/src/app/config/AppConfig.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { loadEnv } from '../../core/config/env'; + +const boolFromEnv = (defaultValue: 'true' | 'false') => + z + .string() + .optional() + .default(defaultValue) + .transform((v) => v === 'true'); + +export const AppConfigSchema = z.object({ + JELLYFIN_URL: z.string(), + JELLYFIN_API_KEY: z.string(), + JELLYFIN_USER_ID: z.string(), + TMDB_API_KEY: z.string(), + + LLM_ENABLED: boolFromEnv('false'), + LLM_BASE_URL: z.string().default('http://localhost:11434/v1'), + LLM_API_KEY: z.string().optional().default(''), + LLM_MODEL: z.string().default('llama3'), + + RADARR_ENABLED: boolFromEnv('false'), + RADARR_URL: z.string().optional().default(''), + RADARR_API_KEY: z.string().optional().default(''), + RADARR_QUALITY_PROFILE_ID: z.coerce.number().default(1), + RADARR_ROOT_FOLDER: z.string().default('/movies'), + + CRON_ENABLED: boolFromEnv('false'), + CRON_SCHEDULE: z.string().default('0 3 * * *'), + + API_ENABLED: z + .string() + .optional() + .default('true') + .transform((v) => v !== 'false'), + API_PORT: z.coerce.number().default(8080), + SECRET_KEY: z.string().optional().default(''), + + RECOMMEND_COUNT: z.coerce.number().default(10), + RECOMMEND_MAX_RETRY: z.coerce.number().default(3), + MIN_RATED_MOVIES: z.coerce.number().default(5), + + LANGFUSE_PUBLIC_KEY: z.string(), + LANGFUSE_SECRET_KEY: z.string(), + LANGFUSE_BASE_URL: z.string(), + LANGFUSE_PROMPT_CACHE_ENABLED: boolFromEnv('true'), +}); + +export type AppConfig = z.infer; + +export function loadAppConfig(source: NodeJS.ProcessEnv = process.env): AppConfig { + return loadEnv(AppConfigSchema, source); +} diff --git a/src/app/models.ts b/src/app/models.ts new file mode 100644 index 0000000..1960c8a --- /dev/null +++ b/src/app/models.ts @@ -0,0 +1,29 @@ +export interface Movie { + name: string; + year: number; + imdbId: string; + tmdbId: string; + genres: string[]; + communityRating: number; + isFavorite: boolean; + playCount: number; +} + +export interface TMDbMovie { + id: number; + title: string; + year: number; +} + +export interface MovieSuggestion { + title: string; + year: number; + reason: string; +} + +export interface Recommendation { + title: string; + year: number; + tmdbId: number; + reason: string; +} diff --git a/test/app/config/AppConfig.test.ts b/test/app/config/AppConfig.test.ts new file mode 100644 index 0000000..ac946cc --- /dev/null +++ b/test/app/config/AppConfig.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { loadAppConfig } from '../../../src/app/config/AppConfig'; + +const REQUIRED_ENV = { + JELLYFIN_URL: 'http://jellyfin', + JELLYFIN_API_KEY: 'jf-key', + JELLYFIN_USER_ID: 'user-1', + TMDB_API_KEY: 'tmdb-key', + LANGFUSE_PUBLIC_KEY: 'lf-pub', + LANGFUSE_SECRET_KEY: 'lf-secret', + LANGFUSE_BASE_URL: 'http://langfuse', +}; + +describe('loadAppConfig', () => { + it('applies defaults for optional settings', () => { + const config = loadAppConfig(REQUIRED_ENV); + expect(config.LLM_ENABLED).toBe(false); + expect(config.RADARR_ENABLED).toBe(false); + expect(config.API_PORT).toBe(8080); + expect(config.RECOMMEND_COUNT).toBe(10); + expect(config.MIN_RATED_MOVIES).toBe(5); + expect(config.LANGFUSE_PROMPT_CACHE_ENABLED).toBe(true); + }); + + it('parses boolean and numeric overrides from env strings', () => { + const config = loadAppConfig({ + ...REQUIRED_ENV, + LLM_ENABLED: 'true', + RADARR_ENABLED: 'true', + API_PORT: '9090', + RECOMMEND_COUNT: '20', + LANGFUSE_PROMPT_CACHE_ENABLED: 'false', + }); + expect(config.LLM_ENABLED).toBe(true); + expect(config.RADARR_ENABLED).toBe(true); + expect(config.API_PORT).toBe(9090); + expect(config.RECOMMEND_COUNT).toBe(20); + expect(config.LANGFUSE_PROMPT_CACHE_ENABLED).toBe(false); + }); + + it('throws when a required var is missing', () => { + const { JELLYFIN_URL, ...rest } = REQUIRED_ENV; + expect(() => loadAppConfig(rest)).toThrow(/Invalid environment configuration/); + }); +});