import { z, ZodError } from "zod"; import { parse } from "@std/toml"; export const ConfigSchema = z.object({ server: z.object({ port: z.number().default(Number(Deno.env.get("PORT")) || 8282), host: z.string().default(Deno.env.get("HOST") || "127.0.0.1"), use_unix_socket: z.boolean().default( Deno.env.get("SERVER_USE_UNIX_SOCKET") === "true" || false, ), unix_socket_path: z.string().default( Deno.env.get("SERVER_UNIX_SOCKET_PATH") || "/tmp/invidious-companion.sock", ), base_path: z.string() .default(Deno.env.get("SERVER_BASE_PATH") || "/companion") .refine( (path) => path.startsWith("/"), { message: "SERVER_BASE_PATH must start with a forward slash (/). Example: '/companion'", }, ) .refine( (path) => !path.endsWith("/") || path === "/", { message: "SERVER_BASE_PATH must not end with a forward slash (/) unless it's the root path. Example: '/companion' not '/companion/'", }, ) .refine( (path) => !path.includes("//"), { message: "SERVER_BASE_PATH must not contain double slashes (//). Example: '/companion' not '//companion' or '/comp//anion'", }, ), secret_key: z.preprocess( (val) => val === undefined ? Deno.env.get("SERVER_SECRET_KEY") || "" : val, z.string().min(1), ).default(undefined), verify_requests: z.boolean().default( Deno.env.get("SERVER_VERIFY_REQUESTS") === "true" || false, ), encrypt_query_params: z.boolean().default( Deno.env.get("SERVER_ENCRYPT_QUERY_PARAMS") === "true" || false, ), enable_metrics: z.boolean().default( Deno.env.get("SERVER_ENABLE_METRICS") === "true" || false, ), }).strict().default({}), cache: z.object({ enabled: z.boolean().default( Deno.env.get("CACHE_ENABLED") === "false" ? false : true, ), directory: z.string().default( Deno.env.get("CACHE_DIRECTORY") || "/var/tmp", ), }).strict().default({}), networking: z.object({ proxy: z.string().nullable().default(Deno.env.get("PROXY") || null), auto_proxy: z.boolean().default( Deno.env.get("NETWORKING_AUTO_PROXY") === "true" || false, ), vpn_source: z.number().default( Number(Deno.env.get("NETWORKING_VPN_SOURCE")) || 1, ), ipv6_block: z.string().nullable().default( Deno.env.get("NETWORKING_IPV6_BLOCK") || null, ), fetch: z.object({ timeout_ms: z.number().default( Number(Deno.env.get("NETWORKING_FETCH_TIMEOUT_MS")) || 30_000, ), retry: z.object({ enabled: z.boolean().default( Deno.env.get("NETWORKING_FETCH_RETRY_ENABLED") === "true" || false, ), times: z.number().optional().default( Number(Deno.env.get("NETWORKING_FETCH_RETRY_TIMES")) || 1, ), initial_debounce: z.number().optional().default( Number( Deno.env.get("NETWORKING_FETCH_RETRY_INITIAL_DEBOUNCE"), ) || 0, ), debounce_multiplier: z.number().optional().default( Number( Deno.env.get( "NETWORKING_FETCH_RETRY_DEBOUNCE_MULTIPLIER", ), ) || 0, ), }).strict().default({}), }).strict().default({}), videoplayback: z.object({ ump: z.boolean().default( Deno.env.get("NETWORKING_VIDEOPLAYBACK_UMP") === "true" || false, ), video_fetch_chunk_size_mb: z.number().default( Number( Deno.env.get( "NETWORKING_VIDEOPLAYBACK_VIDEO_FETCH_CHUNK_SIZE_MB", ), ) || 5, ), }).strict().default({}), }).strict().default({}), jobs: z.object({ youtube_session: z.object({ po_token_enabled: z.boolean().default( Deno.env.get("JOBS_YOUTUBE_SESSION_PO_TOKEN_ENABLED") === "false" ? false : true, ), frequency: z.string().default( Deno.env.get("JOBS_YOUTUBE_SESSION_FREQUENCY") || "*/5 * * * *", ), }).strict().default({}), }).strict().default({}), youtube_session: z.object({ oauth_enabled: z.boolean().default( Deno.env.get("YOUTUBE_SESSION_OAUTH_ENABLED") === "true" || false, ), cookies: z.string().default( Deno.env.get("YOUTUBE_SESSION_COOKIES") || "", ), }).strict().default({}), }).strict(); export type Config = z.infer; export async function parseConfig() { const configFileName = Deno.env.get("CONFIG_FILE") || "config/config.toml"; const configFileContents = await Deno.readTextFile(configFileName).catch( () => null, ); if (configFileContents) { console.log("[INFO] Using custom settings local file"); } else { console.log( "[INFO] No local config file found, using default config", ); } try { const rawConfig = configFileContents ? parse(configFileContents) : {}; const validatedConfig = ConfigSchema.parse(rawConfig); console.log("Loaded Configuration", validatedConfig); return validatedConfig; } catch (err) { let errorMessage = "There is an error in your configuration, check your environment variables"; if (configFileContents) { errorMessage += ` or in your configuration file located at ${configFileName}`; } console.log(errorMessage); if (err instanceof ZodError) { console.log(err.issues); // Include detailed error information in the thrown error for testing const detailedMessage = err.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}` ).join("; "); throw new Error( `Failed to parse configuration file: ${detailedMessage}`, ); } // rethrow error if not Zod throw err; } }