wetvlo 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/app.ts","../src/state/state-manager.ts","../src/types/state.types.ts","../src/app-context.ts","../src/config/config-loader.ts","../src/errors/custom-errors.ts","../src/utils/env-resolver.ts","../src/config/config-schema.ts","../src/utils/deep-merge.ts","../src/utils/url-utils.ts","../src/config/config-defaults.ts","../src/config/config-registry.ts","../src/downloader/download-manager.ts","../src/utils/filename-sanitizer.ts","../src/utils/video-validator.ts","../src/utils/logger.ts","../src/downloader/download-options.ts","../src/downloader/impl/yt-dlp-downloader.ts","../src/downloader/base-downloader.ts","../src/downloader/downloader-registry.ts","../src/handlers/handler-registry.ts","../src/handlers/base/base-handler.ts","../src/handlers/impl/iqiyi-handler.ts","../src/handlers/impl/mgtv-handler.ts","../src/handlers/impl/wetv-handler.ts","../src/notifications/console-notifier.ts","../src/notifications/telegram-notifier.ts","../src/queue/queue-manager.ts","../src/queue/typed-queue.ts","../src/queue/universal-scheduler.ts","../src/utils/time-utils.ts","../src/scheduler/scheduler.ts","../src/utils/cookie-extractor.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { run } from 'cmd-ts';\nimport { cli } from './app.js';\n\n/**\n * Main entry point\n */\nexport async function main(args: string[] = process.argv.slice(2)): Promise<void> {\n await run(cli, args);\n}\n\n// Check if running directly in Node.js or Bun\nconst isMainModule = import.meta.main || (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url));\n\nif (isMainModule) {\n await main();\n}\n","import * as readline from 'node:readline';\nimport { boolean, command, flag, option, string } from 'cmd-ts';\nimport { AppContext } from './app-context.js';\nimport { loadConfig } from './config/config-loader.js';\nimport { ConfigRegistry } from './config/config-registry.js';\nimport type { SeriesConfig } from './config/config-schema.js';\nimport { DownloadManager } from './downloader/download-manager.js';\nimport { ConfigError } from './errors/custom-errors.js';\nimport { handlerRegistry } from './handlers/handler-registry.js';\nimport { IQiyiHandler } from './handlers/impl/iqiyi-handler.js';\nimport { MGTVHandler } from './handlers/impl/mgtv-handler.js';\nimport { WeTVHandler } from './handlers/impl/wetv-handler.js';\nimport { ConsoleNotifier } from './notifications/console-notifier.js';\nimport type { NotificationLevel, Notifier } from './notifications/notifier.js';\nimport { TelegramNotifier } from './notifications/telegram-notifier.js';\nimport { Scheduler } from './scheduler/scheduler.js';\nimport { StateManager } from './state/state-manager.js';\nimport type { SchedulerMode, SchedulerOptions } from './types/config.types.js';\nimport { readCookieFile } from './utils/cookie-extractor.js';\nimport { logger } from './utils/logger.js';\n\nexport type AppDependencies = {\n loadConfig: typeof loadConfig;\n checkYtDlpInstalled: () => Promise<boolean>;\n readCookieFile: typeof readCookieFile;\n createDownloadManager: () => DownloadManager;\n createScheduler: (configs: SeriesConfig[], downloadManager: DownloadManager, options?: SchedulerOptions) => Scheduler;\n};\n\nconst defaultDependencies: AppDependencies = {\n loadConfig,\n checkYtDlpInstalled: DownloadManager.checkYtDlpInstalled,\n readCookieFile,\n createDownloadManager: () => new DownloadManager(),\n createScheduler: (c, dm, opt) => new Scheduler(c, dm, opt),\n};\n\n/**\n * Handle graceful shutdown\n */\nexport async function handleShutdown(scheduler: Scheduler): Promise<void> {\n logger.info('Shutting down gracefully...');\n\n try {\n await scheduler.stop();\n logger.success('Shutdown complete');\n } catch (error) {\n logger.error(`Error during shutdown: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nexport async function runApp(\n configPath: string,\n mode: SchedulerMode,\n deps: AppDependencies = defaultDependencies,\n): Promise<void> {\n logger.info(`Mode: ${mode === 'once' ? 'Single-run (checks once, exits)' : 'Scheduled (waits for startTime)'}`);\n\n // Check if yt-dlp is installed\n logger.info('Checking yt-dlp installation...');\n const ytDlpInstalled = await deps.checkYtDlpInstalled();\n\n if (!ytDlpInstalled) {\n throw new Error(\n 'yt-dlp is not installed. Please install it first:\\n' +\n ' - macOS: brew install yt-dlp\\n' +\n ' - Linux: pip install yt-dlp\\n' +\n ' - Windows: winget install yt-dlp',\n );\n }\n\n // Load configuration\n logger.info(`Loading configuration from ${configPath}...`);\n const config = await deps.loadConfig(configPath);\n logger.success('Configuration loaded');\n\n // Create config registry\n const configRegistry = new ConfigRegistry(config);\n\n // Get global config (stored in let for config reload comparison)\n let _globalConfig = configRegistry.getConfig('global');\n\n /**\n * Create notifier instance from config\n * Extracted to factory function for reuse during config reload\n */\n const createNotifier = (registry: ConfigRegistry): Notifier => {\n const notifiers: Array<ConsoleNotifier | TelegramNotifier> = [new ConsoleNotifier()];\n const cfg = registry.getConfig('global');\n\n if (cfg.telegram) {\n try {\n notifiers.push(new TelegramNotifier(cfg.telegram));\n logger.info('Telegram notifications enabled for errors');\n } catch (error) {\n logger.warning(`Failed to set up Telegram: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n // Create composite notifier\n return {\n notify: async (level: NotificationLevel, message: string): Promise<void> => {\n await Promise.all(notifiers.map((n) => n.notify(level, message)));\n },\n progress: (message: string): void => {\n for (const n of notifiers) {\n n.progress(message);\n }\n },\n endProgress: (): void => {\n for (const n of notifiers) {\n n.endProgress();\n }\n },\n };\n };\n\n const notifier = createNotifier(configRegistry);\n\n // Create state manager\n const stateManager = new StateManager(notifier);\n\n // Initialize AppContext with all services\n AppContext.initialize(configRegistry, notifier, stateManager);\n logger.info('AppContext initialized');\n\n // Register handlers\n handlerRegistry.register(new WeTVHandler());\n handlerRegistry.register(new IQiyiHandler());\n handlerRegistry.register(new MGTVHandler());\n logger.info(`Registered handlers: ${handlerRegistry.getDomains().join(', ')}`);\n\n // Create download manager\n const downloadManager = deps.createDownloadManager();\n\n // Setup interactive mode instructions\n let onIdle: (() => void) | undefined;\n if (mode === 'scheduled' && process.stdin.isTTY) {\n const printInstructions = () => {\n logger.info('Interactive mode enabled:');\n logger.info(' [r] Reload configuration');\n logger.info(' [c] Trigger immediate checks');\n logger.info(' [q] Quit');\n };\n\n onIdle = printInstructions;\n }\n\n // Create and start scheduler with queue-based architecture\n logger.info('Using queue-based scheduler');\n const scheduler = deps.createScheduler(config.series, downloadManager, { mode, onIdle });\n\n // Set up signal handlers for graceful shutdown\n process.on('SIGINT', async () => {\n await handleShutdown(scheduler);\n process.exit(0);\n });\n process.on('SIGTERM', async () => {\n await handleShutdown(scheduler);\n process.exit(0);\n });\n\n // Setup keyboard input listeners\n if (mode === 'scheduled' && process.stdin.isTTY) {\n readline.emitKeypressEvents(process.stdin);\n process.stdin.setRawMode(true);\n\n process.stdin.on('keypress', async (_str, key) => {\n if (!key) return;\n\n const name = key.name || '';\n\n // q, й or Ctrl+C to quit\n if (name === 'q' || name === 'й' || (key.ctrl && name === 'c')) {\n await handleShutdown(scheduler);\n process.exit(0);\n }\n // r or к to reload config\n else if (name === 'r' || name === 'к') {\n try {\n logger.info(`Reloading configuration from ${configPath}...`);\n const newConfig = await deps.loadConfig(configPath);\n const newConfigRegistry = new ConfigRegistry(newConfig);\n const newGlobalConfig = newConfigRegistry.getConfig('global');\n\n // Reload notifier (Telegram settings, etc.)\n const newNotifier = createNotifier(newConfigRegistry);\n AppContext.setNotifier(newNotifier);\n\n // Update global config reference\n _globalConfig = newGlobalConfig;\n\n // Reload config registry and scheduler\n AppContext.reloadConfig(newConfigRegistry);\n await scheduler.reload(newConfig.series);\n\n logger.success('Configuration reloaded successfully');\n } catch (error) {\n logger.error(`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n // c or с (Cyrillic) to trigger checks\n else if (name === 'c' || name === 'с') {\n await scheduler.triggerAllChecks();\n }\n });\n }\n\n // Start the scheduler\n await scheduler.start();\n}\n\n// Define CLI using cmd-ts\nexport const cli = command({\n name: 'wetvlo',\n description: 'CLI Video Downloader for Chinese streaming sites',\n version: '0.0.1',\n args: {\n config: option({\n type: string,\n long: 'config',\n short: 'c',\n defaultValue: () => './config.yaml',\n description: 'Path to configuration file (default: ./config.yaml)',\n }),\n once: flag({\n type: boolean,\n long: 'once',\n short: 'o',\n description: 'Run in single-run mode (check once and exit)',\n }),\n },\n handler: async ({ config, once }: { config: string; once: boolean }) => {\n try {\n const mode: SchedulerMode = once ? 'once' : 'scheduled';\n await runApp(config, mode);\n } catch (error) {\n if (error instanceof ConfigError) {\n logger.error(`Configuration error: ${error.message}`);\n } else {\n logger.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`);\n }\n process.exit(1);\n }\n },\n});\n","import { existsSync, readFileSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\nimport type { Notifier } from '../notifications/notifier';\nimport { NotificationLevel } from '../notifications/notifier';\nimport type { State } from '../types/state.types';\nimport { createEmptyState } from '../types/state.types';\n\n/**\n * Episode number type (zero-padded string, e.g., \"01\", \"02\")\n */\nexport type EpisodeNumber = string;\n\n/**\n * State manager class for tracking downloaded episodes (v3.0.0 - file-based)\n *\n * This implementation does NOT keep state in memory. Every operation reads/writes\n * the file directly, with mutex protection to ensure atomicity.\n *\n * Usage:\n * const stateManager = AppContext.getStateManager();\n * const statePath = resolveStatePath(config);\n * stateManager.isDownloaded(statePath, seriesName, episodeNumber);\n * await stateManager.addDownloadedEpisode(statePath, seriesName, episodeNumber);\n */\nexport class StateManager {\n private static locks = new Map<string, Promise<void>>();\n private notifier?: Notifier;\n\n constructor(notifier?: Notifier) {\n this.notifier = notifier;\n }\n\n /**\n * Check if an episode has been downloaded\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @param episodeNumber - Episode number\n * @returns Whether the episode is downloaded\n */\n isDownloaded(statePath: string, seriesName: string, episodeNumber: number): boolean {\n try {\n const state = this.loadState(statePath);\n const episodes = state.series[seriesName];\n if (!episodes) return false;\n\n const paddedNumber = String(episodeNumber).padStart(2, '0');\n return episodes.includes(paddedNumber);\n } catch (error) {\n this.handleError(error, `Failed to check episode status for ${seriesName}`);\n return false;\n }\n }\n\n /**\n * Add a downloaded episode to state (atomic operation: read → modify → write)\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @param episodeNumber - Episode number\n */\n async addDownloadedEpisode(statePath: string, seriesName: string, episodeNumber: number): Promise<void> {\n return this.withLock(statePath, async () => {\n try {\n const state = this.loadState(statePath);\n\n if (!state.series[seriesName]) {\n state.series[seriesName] = [];\n }\n\n const episodeStr = String(episodeNumber).padStart(2, '0');\n if (!state.series[seriesName].includes(episodeStr)) {\n state.series[seriesName].push(episodeStr);\n state.series[seriesName].sort();\n }\n\n await this.saveState(statePath, state);\n } catch (error) {\n this.handleError(error, `Failed to add episode for ${seriesName}`);\n throw error;\n }\n });\n }\n\n /**\n * Get all episodes for a series\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @returns Array of episode numbers (as zero-padded strings)\n */\n getSeriesEpisodes(statePath: string, seriesName: string): EpisodeNumber[] {\n try {\n const state = this.loadState(statePath);\n return state.series[seriesName] ?? [];\n } catch (error) {\n this.handleError(error, `Failed to get episodes for ${seriesName}`);\n return [];\n }\n }\n\n /**\n * Execute a function with mutex lock for a specific state file\n *\n * @param statePath - Path to state file (used as lock key)\n * @param fn - Function to execute while holding the lock\n * @returns Result of the function\n */\n private async withLock<T>(statePath: string, fn: () => Promise<T>): Promise<T> {\n // Wait for previous operation to complete\n let currentLock = StateManager.locks.get(statePath);\n while (currentLock) {\n await currentLock;\n currentLock = StateManager.locks.get(statePath);\n }\n\n // Create a new lock\n const lockPromise = (async () => {\n try {\n return await fn();\n } finally {\n StateManager.locks.delete(statePath);\n }\n })();\n\n // @ts-expect-error - T extends void is guaranteed by usage\n StateManager.locks.set(statePath, lockPromise);\n return lockPromise;\n }\n\n /**\n * Load state from file\n *\n * @param statePath - Path to state file (relative or absolute)\n * @returns State object\n */\n private loadState(statePath: string): State {\n const fullPath = this.resolvePath(statePath);\n\n if (!existsSync(fullPath)) {\n return createEmptyState();\n }\n\n try {\n // Use synchronous read for isDownloaded (non-async method)\n const fileContent = readFileSync(fullPath, 'utf-8');\n return JSON.parse(fileContent) as State;\n } catch (error) {\n throw new Error(\n `Failed to load state from ${fullPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Save state to file\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param state - State object to save\n */\n private async saveState(statePath: string, state: State): Promise<void> {\n const fullPath = this.resolvePath(statePath);\n\n try {\n // Sort series keys and episode numbers\n const sortedSeries: Record<string, string[]> = {};\n Object.keys(state.series)\n .sort()\n .forEach((key) => {\n const episodes = state.series[key];\n if (episodes) {\n sortedSeries[key] = [...episodes].sort();\n }\n });\n\n state.series = sortedSeries;\n\n const content = JSON.stringify(state, null, 2);\n await writeFile(fullPath, content, 'utf-8');\n } catch (error) {\n throw new Error(`Failed to save state to ${fullPath}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n /**\n * Resolve state path to absolute path\n *\n * @param statePath - Path to state file (relative or absolute)\n * @returns Absolute path\n */\n private resolvePath(statePath: string): string {\n // If already absolute, return as-is\n if (isAbsolute(statePath)) {\n return statePath;\n }\n // Otherwise, resolve relative to current working directory\n return join(process.cwd(), statePath);\n }\n\n /**\n * Handle errors through notifier\n *\n * @param error - Error object\n * @param message - Error message prefix\n */\n private handleError(error: unknown, message: string): void {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const fullMessage = `${message}: ${errorMessage}`;\n\n if (this.notifier) {\n this.notifier.notify(NotificationLevel.ERROR, fullMessage);\n } else {\n console.error(fullMessage);\n }\n }\n}\n","/**\n * State file structure (v3.0.0)\n */\nexport type State = {\n /** State format version */\n version: string;\n /** Series keyed by Series Name, values are sorted lists of episode numbers (e.g., \"01\") */\n series: Record<string, string[]>;\n};\n\n/**\n * Create a new empty state (v3.0.0)\n */\nexport function createEmptyState(): State {\n return {\n version: '3.0.0',\n series: {},\n };\n}\n","/**\n * Global application context\n *\n * Provides centralized access to shared services (config, notifier).\n * Eliminates the need to pass these dependencies through multiple layers.\n *\n * Usage:\n * 1. Initialize early in app startup: AppContext.initialize(...)\n * 2. Access anywhere: import { AppContext } from './app-context'\n */\n\nimport type { ConfigRegistry } from './config/config-registry.js';\nimport type { Notifier } from './notifications/notifier.js';\nimport { StateManager } from './state/state-manager.js';\n\n/**\n * Global application context singleton\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: Intentional singleton pattern for global app context\nexport class AppContext {\n private static configRegistry?: ConfigRegistry;\n private static notifier?: Notifier;\n private static stateManager?: StateManager;\n\n /**\n * Initialize the application context with pre-created services\n *\n * Called once during app startup to set up shared services.\n *\n * @param configRegistry - Config registry instance\n * @param notifier - Notifier instance\n * @param stateManager - State manager instance (optional, created from notifier if not provided)\n */\n static initialize(configRegistry: ConfigRegistry, notifier: Notifier, stateManager?: StateManager): void {\n AppContext.configRegistry = configRegistry;\n AppContext.notifier = notifier;\n AppContext.stateManager = stateManager || (notifier ? new StateManager(notifier) : undefined);\n }\n\n /**\n * Get the config registry instance\n *\n * @throws Error if context not initialized\n */\n static getConfig(): ConfigRegistry {\n if (!AppContext.configRegistry) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.configRegistry;\n }\n\n /**\n * Get the notifier instance\n *\n * @throws Error if context not initialized\n */\n static getNotifier(): Notifier {\n if (!AppContext.notifier) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.notifier;\n }\n\n /**\n * Get the state manager instance\n *\n * @throws Error if context not initialized\n */\n static getStateManager(): StateManager {\n if (!AppContext.stateManager) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.stateManager;\n }\n\n /**\n * Reload configuration\n *\n * Updates the ConfigRegistry with new configuration.\n * Useful for runtime config reloading.\n *\n * @param configRegistry - New config registry instance\n */\n static reloadConfig(configRegistry: ConfigRegistry): void {\n AppContext.configRegistry = configRegistry;\n }\n\n /**\n * Update the notifier instance\n *\n * Useful for hot-swapping notifiers (e.g., adding Telegram).\n *\n * @param notifier - New notifier instance\n */\n static setNotifier(notifier: Notifier): void {\n AppContext.notifier = notifier;\n }\n\n /**\n * Check if context is initialized\n */\n static isInitialized(): boolean {\n return AppContext.configRegistry !== undefined;\n }\n\n /**\n * Reset the context (useful for testing)\n */\n static reset(): void {\n AppContext.configRegistry = undefined;\n AppContext.notifier = undefined;\n AppContext.stateManager = undefined;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as yaml from 'js-yaml';\nimport type { Config, RawConfig } from '../config/config-schema.js';\nimport { ConfigError } from '../errors/custom-errors';\nimport { resolveEnvRecursive } from '../utils/env-resolver';\nimport { validateConfig } from './config-schema';\n\n/**\n * Default config file path\n */\nexport const DEFAULT_CONFIG_PATH = './config.yaml';\n\n/**\n * Load and parse configuration from YAML file\n *\n * @param configPath - Path to config file\n * @returns Parsed configuration\n * @throws ConfigError if file doesn't exist or is invalid\n */\nexport async function loadConfig(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n // Resolve relative path\n const absolutePath = join(process.cwd(), configPath);\n\n if (!existsSync(absolutePath)) {\n throw new ConfigError(\n `Configuration file not found: \"${absolutePath}\". Create a config.yaml file or specify a different path.`,\n );\n }\n\n const content = await readFile(absolutePath, 'utf-8');\n\n let rawConfig: RawConfig;\n\n try {\n rawConfig = yaml.load(content) as RawConfig;\n } catch (error) {\n throw new ConfigError(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n // Validate configuration structure\n validateConfig(rawConfig);\n\n // Resolve environment variables\n const config = resolveEnvRecursive(rawConfig) as unknown as Config;\n\n return config;\n}\n\n/**\n * Load config with defaults for optional fields\n */\nexport async function loadConfigWithDefaults(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n return loadConfig(configPath);\n}\n","/**\n * Base error class for wetvlo\n */\nexport class WetvloError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'WetvloError';\n }\n}\n\n/**\n * Configuration error\n */\nexport class ConfigError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * State file error\n */\nexport class StateError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'StateError';\n }\n}\n\n/**\n * Handler error (episode extraction issues)\n */\nexport class HandlerError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'HandlerError';\n }\n}\n\n/**\n * Download error\n */\nexport class DownloadError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'DownloadError';\n }\n}\n\n/**\n * Notification error\n */\nexport class NotificationError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'NotificationError';\n }\n}\n\n/**\n * Cookie extraction error\n */\nexport class CookieError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'CookieError';\n }\n}\n\n/**\n * Scheduling error\n */\nexport class SchedulerError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'SchedulerError';\n }\n}\n","/**\n * Resolve environment variables in strings\n * Supports ${VAR_NAME} syntax\n *\n * @param value - String that may contain ${VAR_NAME} placeholders\n * @returns String with environment variables resolved\n */\nexport function resolveEnv(value: string): string {\n if (typeof value !== 'string') {\n return value;\n }\n\n return value.replace(/\\$\\{([^}]+)\\}/g, (_match, varName) => {\n const envValue = process.env[varName];\n if (envValue === undefined) {\n throw new Error(`Environment variable \"${varName}\" is not set`);\n }\n return envValue;\n });\n}\n\n/**\n * Recursively resolve environment variables in object\n *\n * @param obj - Object that may contain strings with ${VAR_NAME}\n * @returns Object with all environment variables resolved\n */\nexport function resolveEnvRecursive<T>(obj: T): T {\n if (typeof obj === 'string') {\n return resolveEnv(obj) as T;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => resolveEnvRecursive(item)) as T;\n }\n\n if (obj !== null && typeof obj === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[key] = resolveEnvRecursive(value);\n }\n return result as T;\n }\n\n return obj;\n}\n","/**\n * Zod schemas for configuration validation\n *\n * This file defines both the validation schemas AND the TypeScript types.\n * Types are automatically inferred from the schemas, ensuring they stay in sync.\n */\n\nimport { z } from 'zod';\nimport type { DeepMerge } from '../utils/deep-merge';\nimport type { DefaultConfig } from './config-defaults';\n\n/**\n * Episode types\n */\nconst EpisodeTypeSchema = z.enum(['available', 'vip', 'svip', 'teaser', 'express', 'preview', 'locked']);\n\nexport type EpisodeType = z.infer<typeof EpisodeTypeSchema>;\n/**\n * Check settings for series/domain\n */\nexport const CheckSettingsSchema = z.object({\n count: z.number().positive().optional().describe('Number of episodes to check'),\n checkInterval: z.number().positive().optional().describe('Interval between checks in seconds'),\n downloadTypes: z.array(EpisodeTypeSchema).optional().describe('Episode types to download'),\n});\n\nexport type CheckSettings = z.infer<typeof CheckSettingsSchema>;\n\nexport type CheckSettingsResolved = DeepMerge<DefaultConfig['check'], CheckSettings>;\n/**\n * Download settings for series/domain\n */\nexport const DownloadSettingsSchema = z.object({\n downloadDir: z.string().optional().describe('Directory to save downloaded episodes'),\n tempDir: z.string().optional().describe('Directory for temporary files'),\n downloadDelay: z.number().nonnegative().optional().describe('Delay between downloads in milliseconds'),\n maxRetries: z.number().int().nonnegative().optional().describe('Maximum number of retry attempts'),\n initialTimeout: z.number().positive().optional().describe('Initial timeout for operations in milliseconds'),\n backoffMultiplier: z.number().positive().optional().describe('Multiplier for exponential backoff'),\n jitterPercentage: z.number().int().min(0).max(100).optional().describe('Jitter percentage for retry delays'),\n minDuration: z.number().nonnegative().optional().describe('Minimum duration in seconds for downloads'),\n});\n\nexport type DownloadSettings = z.infer<typeof DownloadSettingsSchema>;\n\nexport type DownloadSettingsResolved = DeepMerge<DefaultConfig['download'], DownloadSettings>;\n\n/**\n * Telegram notification configuration\n */\nexport const TelegramConfigSchema = z.object({\n botToken: z.string().describe('Telegram bot token'),\n chatId: z.string().describe('Telegram chat ID'),\n});\n\nexport type TelegramConfig = z.infer<typeof TelegramConfigSchema>;\n\n/**\n * Browser options\n */\nconst BrowserSchema = z.enum(['chrome', 'firefox', 'safari', 'chromium', 'edge']);\n\nconst CommonSettingsSchema = z.object({\n check: CheckSettingsSchema.optional().describe('Check settings'),\n download: DownloadSettingsSchema.optional().describe('Download settings'),\n telegram: TelegramConfigSchema.optional().describe('Telegram notification configuration'),\n stateFile: z.string().optional().describe('Path to state file'),\n browser: BrowserSchema.optional().describe('Browser to use for scraping'),\n cookieFile: z.string().optional().describe('Path to cookie file'),\n});\n\n/**\n * Global configuration defaults\n */\nexport const GlobalConfigSchema = CommonSettingsSchema;\n\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n\nexport type GlobalConfigResolved = DeepMerge<DefaultConfig, GlobalConfig>;\n\n/**\n * Domain-specific configuration\n */\nexport const DomainConfigSchema = CommonSettingsSchema.extend({\n domain: z.string().describe('Domain name (e.g., \"weTV\")'),\n});\n\nexport type DomainConfig = z.infer<typeof DomainConfigSchema>;\n\nexport type DomainConfigResolved = DeepMerge<GlobalConfigResolved, DomainConfig>;\n\n/**\n * Series configuration\n */\nexport const SeriesConfigSchema = CommonSettingsSchema.extend({\n name: z.string().describe('Series name'),\n url: z.url().describe('Series URL'),\n startTime: z\n .string()\n .regex(/^\\d{1,2}:\\d{2}$/, {\n message: 'Must be in HH:MM format (e.g., \"20:00\")',\n })\n .optional()\n .describe('Start time in HH:MM format'),\n cron: z.string().optional().describe('Cron expression for scheduling'),\n});\n\nexport type SeriesConfig = z.infer<typeof SeriesConfigSchema>;\n\nexport type SeriesConfigResolved = DeepMerge<DomainConfigResolved, SeriesConfig>;\n\n/**\n * Main configuration schema\n */\nexport const ConfigSchema = z.object({\n series: z.array(SeriesConfigSchema).min(1, 'Cannot be empty').describe('List of series to monitor'),\n domainConfigs: z.array(DomainConfigSchema).optional().describe('Domain-specific configurations'),\n globalConfig: GlobalConfigSchema.optional().describe('Global configuration defaults'),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Raw configuration before env var resolution\n */\nexport type RawConfig = Record<string, unknown>;\n\n/**\n * Configuration level for resolution\n */\nexport type Level = 'global' | 'domain' | 'series';\n\n/**\n * Resolved configuration type based on level\n */\nexport type ResolvedConfig<L extends Level> = L extends 'global'\n ? GlobalConfigResolved\n : L extends 'domain'\n ? DomainConfigResolved\n : SeriesConfigResolved;\n\n/**\n * Validate configuration using Zod\n *\n * @param rawConfig - Raw configuration object from YAML\n * @throws z.ZodError if validation fails\n */\nexport function validateConfig(rawConfig: RawConfig): void {\n ConfigSchema.parse(rawConfig);\n}\n\n/**\n * Validate with custom error formatting\n *\n * @param rawConfig - Raw configuration object\n * @returns Object with { success: boolean, error?: string }\n */\nexport function validateConfigSafe(rawConfig: RawConfig): { success: true } | { success: false; error: string } {\n try {\n ConfigSchema.parse(rawConfig);\n return { success: true };\n } catch (error) {\n if (error instanceof z.ZodError) {\n return { success: false, error: formatZodError(error) };\n }\n return { success: false, error: String(error) };\n }\n}\n\n/**\n * Format Zod error into a readable message\n */\nfunction formatZodError(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? `\"${issue.path.join('.')}\"` : 'value';\n const code = issue.code.toUpperCase();\n return `${path} ${issue.message} [${code}]`;\n })\n .join('; ');\n}\n","export function deepMerge<T extends object, U extends object>(target: T, source?: U): DeepMerge<T, U> {\n if (!source) {\n return target as unknown as DeepMerge<T, U>;\n }\n\n const result = { ...target } as Record<string, unknown>;\n\n for (const key in source) {\n if (Object.hasOwn(source, key)) {\n const sourceValue = source[key];\n const targetValue = result[key];\n\n if (isObject(sourceValue) && isObject(targetValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n }\n\n return result as DeepMerge<T, U>;\n}\n\nfunction isObject(item: unknown): item is object {\n return typeof item === 'object' && item !== null && !Array.isArray(item);\n}\n\n// 1. Utility for forced type disclosure (nice output in IDE)\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n// 2. Smart check for object/Record\n// Exclude arrays and functions, consider the possibility of undefined\n// biome-ignore lint/complexity/noBannedTypes: type check\n// biome-ignore lint/suspicious/noExplicitAny: type check\ntype IsRecord<T> = T extends object ? (T extends any[] ? false : T extends Function ? false : true) : false;\n\n// Helper for obtaining a pure type without undefined\ntype NotUndefined<T> = Exclude<T, undefined>;\n\n// 3. Choosing keys\ntype OptionalKeys<T> = {\n // biome-ignore lint/complexity/noBannedTypes: type check\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\n\n// 4. Merge values\ntype MergeValues<T, S> =\n // Check if both values (without undefined) are objects\n IsRecord<NotUndefined<T>> extends true\n ? IsRecord<NotUndefined<S>> extends true\n ? // IF BOTH ARE OBJECTS:\n // Recursively merge their \"clean\" versions.\n // IMPORTANT: We removed `| (undefined extends S ? T : never)`,\n // to prevent the strict type from Source from being swallowed by the weak type from Target.\n DeepMerge<NotUndefined<T>, NotUndefined<S>>\n : // IF DIFFERENT TYPES (or primitives):\n SimpleMerge<T, S>\n : SimpleMerge<T, S>;\n\n// 5. Simple merge for primitives\n// Here we leave the fallback to T, as it's safe for primitives (string | undefined)\ntype SimpleMerge<T, S> = NotUndefined<S> | (undefined extends S ? T : never);\n\n// 6. Main type DeepMerge\nexport type DeepMerge<T, S> =\n IsRecord<NotUndefined<T>> extends true\n ? IsRecord<NotUndefined<S>> extends true\n ? Simplify<\n // Keys from T (that are not in S)\n Pick<T, Exclude<keyof T, keyof S>> &\n // Keys from S (that are not in T)\n Pick<S, Exclude<keyof S, keyof T>> & {\n // If a key is required in AT LEAST ONE object -> it's required // Common keys:\n [K in (RequiredKeys<T> & keyof S) | (RequiredKeys<S> & keyof T)]: MergeValues<T[K], S[K]>;\n // biome-ignore lint/suspicious/noExplicitAny: type check\n } & { [K in (OptionalKeys<T> & OptionalKeys<S>) & keyof any]?: MergeValues<T[K], S[K]> } // If a key is optional in BOTH -> it's optional\n >\n : S // If S is no longer an object, it overwrites T\n : S;\n","/**\n * Extract domain from URL\n *\n * @param url - URL to extract domain from\n * @returns Domain (e.g., \"wetv.vip\")\n */\nexport function extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n throw new Error(`Invalid URL: \"${url}\"`);\n }\n}\n\n/**\n * Check if URL is valid\n *\n * @param url - URL to validate\n * @returns True if URL is valid\n */\nexport function isValidUrl(url: string): boolean {\n try {\n new URL(url);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Normalize URL by removing trailing slash and fragment\n *\n * @param url - URL to normalize\n * @returns Normalized URL\n */\nexport function normalizeUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n urlObj.hash = '';\n // Remove trailing slash from pathname\n if (urlObj.pathname.endsWith('/')) {\n urlObj.pathname = urlObj.pathname.slice(0, -1);\n }\n return urlObj.toString();\n } catch {\n return url;\n }\n}\n","type EpisodeType = 'available' | 'vip' | 'svip' | 'teaser' | 'express' | 'preview' | 'locked';\n\nexport type DefaultConfig = {\n stateFile: string;\n browser?: string;\n cookieFile?: string;\n\n check: {\n count: number;\n checkInterval: number;\n downloadTypes: EpisodeType[];\n };\n\n download: {\n downloadDir: string;\n tempDir: string;\n downloadDelay: number;\n maxRetries: number;\n initialTimeout: number;\n backoffMultiplier: number;\n jitterPercentage: number;\n minDuration: number;\n };\n\n telegram?: {\n botToken: string;\n chatId: string;\n };\n};\n\nexport const defaults: DefaultConfig = {\n check: {\n count: 3,\n checkInterval: 600,\n downloadTypes: ['available'],\n },\n download: {\n downloadDir: './downloads',\n tempDir: './downloads',\n downloadDelay: 10,\n maxRetries: 3,\n initialTimeout: 5,\n backoffMultiplier: 2,\n jitterPercentage: 10,\n minDuration: 0,\n },\n stateFile: 'wetvlo-state.json',\n browser: 'chrome',\n};\n\n/**\n * Default configuration values\n */\nexport function getDefaults() {\n return defaults;\n}\n","/**\n * ConfigRegistry - Centralized configuration registry with pre-merged configs\n *\n * Merges configuration at construction time:\n * defaults → global → domain → series\n *\n * Simplified API:\n * - registry.resolve(url) - Get resolved config for a series URL\n * - registry.resolve(url, \"domain\") - Get domain-level config\n * - registry.resolve(url, \"global\") - Get global-level config\n */\n\nimport { deepMerge } from '../utils/deep-merge.js';\nimport { extractDomain } from '../utils/url-utils.js';\nimport { getDefaults } from './config-defaults.js';\nimport type {\n Config,\n DomainConfigResolved,\n GlobalConfigResolved,\n Level,\n ResolvedConfig,\n SeriesConfigResolved,\n} from './config-schema.js';\n\ntype SeriesKey = `series:${string}`;\n\ntype DomainKye = `domain:${string}`;\n\ntype GlobalKey = 'global';\n\ntype ValidKey = GlobalKey | DomainKye | SeriesKey;\n\n/**\n * Configuration registry with pre-merged configs\n */\nexport class ConfigRegistry {\n private readonly map = new Map<ValidKey, GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved>();\n private readonly seriesByUrl = new Map<string, SeriesConfigResolved>();\n\n /**\n * Create a new ConfigRegistry\n *\n * @param root - Root configuration object\n */\n constructor(root: Config) {\n // Merge at construction time: defaults → global → domain → series\n const defaults = getDefaults();\n\n const globalMerged = deepMerge(defaults, root.globalConfig);\n this.setConfig('global', globalMerged);\n\n // Domain configs\n for (const dc of root.domainConfigs || []) {\n const domainMerged = deepMerge(globalMerged, dc);\n this.setConfig(`domain:${dc.domain}`, domainMerged);\n }\n\n // Series configs\n for (const sc of root.series) {\n const hostname = extractDomain(sc.url);\n let domainMerged = this.getConfig(`domain:${hostname}`);\n if (!domainMerged) {\n const globalConfig = this.getConfig('global');\n domainMerged = deepMerge(globalConfig, { domain: hostname });\n }\n const seriesMerged = deepMerge(domainMerged, sc);\n this.setConfig(`series:${sc.url}`, seriesMerged);\n this.seriesByUrl.set(sc.url, seriesMerged);\n }\n }\n\n getConfig(key: 'global'): GlobalConfigResolved;\n getConfig(key: `domain:${string}`): DomainConfigResolved | undefined;\n getConfig(key: `series:${string}`): SeriesConfigResolved;\n getConfig(key: ValidKey): GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved | undefined {\n return this.map.get(key) as GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved | undefined;\n }\n\n setConfig(key: 'global', config: GlobalConfigResolved): void;\n setConfig(key: `domain:${string}`, config: DomainConfigResolved): void;\n setConfig(key: `series:${string}`, config: SeriesConfigResolved): void;\n setConfig(key: ValidKey, config: GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved): void {\n this.map.set(key, config);\n }\n\n /**\n * Resolve configuration for a URL\n *\n * @param url - Series URL\n * @param level - Resolution level (\"full\", \"domain\", or \"global\")\n * @returns Resolved configuration\n */\n resolve<L extends Level>(url: string, level?: L): ResolvedConfig<L> {\n if (level === 'global') {\n const config = this.getConfig('global');\n if (!config) {\n throw new Error('Global configuration not found');\n }\n return config as ResolvedConfig<L>;\n }\n\n if (level === 'domain') {\n const domain = extractDomain(url);\n const config = this.getConfig(`domain:${domain}`);\n if (!config) {\n // Fall back to global if domain config not found\n const globalConfig = this.getConfig('global');\n if (!globalConfig) {\n throw new Error('Global configuration not found');\n }\n return Object.assign(globalConfig, { domain }) as ResolvedConfig<L>;\n }\n return config as ResolvedConfig<L>;\n }\n\n // Default to \"full\" resolution\n const config = this.getConfig(`series:${url}`);\n if (!config) {\n throw new Error(`No configuration found for URL: ${url}`);\n }\n\n const resolved = config;\n this.validate(resolved);\n return resolved as ResolvedConfig<L>;\n }\n\n /**\n * List all series configurations\n *\n * @returns Array of series configurations\n */\n listSeries(): SeriesConfigResolved[] {\n return Array.from(this.seriesByUrl.values());\n }\n\n /**\n * List all series URLs\n *\n * @returns Array of series URLs\n */\n listSeriesUrls(): string[] {\n return Array.from(this.seriesByUrl.keys());\n }\n\n /**\n * List all configured domains\n *\n * @returns Array of domain names\n */\n listDomains(): string[] {\n const domains = new Set<string>();\n for (const url of this.seriesByUrl.keys()) {\n domains.add(extractDomain(url));\n }\n return Array.from(domains);\n }\n\n /**\n * Validate resolved configuration\n */\n private validate(config: SeriesConfigResolved): void {\n if (!config.check) {\n throw new Error('Missing check configuration');\n }\n if (!config.download) {\n throw new Error('Missing download configuration');\n }\n\n const { check, download } = config;\n\n if (check.count < 1) {\n throw new Error(`Invalid check count: ${check.count}`);\n }\n if (check.checkInterval < 0) {\n throw new Error(`Invalid check interval: ${check.checkInterval}`);\n }\n if (download.downloadDelay < 0) {\n throw new Error(`Invalid download delay: ${download.downloadDelay}`);\n }\n if (download.maxRetries < 0) {\n throw new Error(`Invalid max retries: ${download.maxRetries}`);\n }\n if (download.initialTimeout < 0) {\n throw new Error(`Invalid initial timeout: ${download.initialTimeout}`);\n }\n if (download.backoffMultiplier < 1) {\n throw new Error(`Invalid backoff multiplier: ${download.backoffMultiplier}`);\n }\n if (download.minDuration < 0) {\n throw new Error(`Invalid min duration: ${download.minDuration}`);\n }\n }\n}\n","import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport { AppContext } from '../app-context.js';\nimport { DownloadError } from '../errors/custom-errors';\nimport { NotificationLevel } from '../notifications/notifier';\nimport type { StateManager } from '../state/state-manager';\nimport type { Episode } from '../types/episode.types';\nimport { sanitizeFilename } from '../utils/filename-sanitizer';\nimport * as VideoValidator from '../utils/video-validator';\nimport type { DownloadOptions } from './download-options.js';\nimport { extractDownloadOptions } from './download-options.js';\nimport { downloaderRegistry } from './downloader-registry';\nimport { YtDlpDownloader } from './impl/yt-dlp-downloader';\n\n/**\n * Escape special characters in a string for use in a regular expression\n */\nfunction escapeRegExp(string: string): string {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Download manager with progress tracking\n */\nexport class DownloadManager {\n private stateManager: StateManager;\n\n constructor() {\n // Get StateManager from AppContext\n this.stateManager = AppContext.getStateManager();\n }\n\n /**\n * Download an episode using appropriate downloader\n */\n async download(seriesUrl: string, episode: Episode): Promise<boolean> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n\n // Get resolved config\n const resolved = registry.resolve(seriesUrl, 'series');\n const statePath = resolved.stateFile;\n const seriesName = resolved.name;\n const downloadOptions: DownloadOptions = extractDownloadOptions(resolved);\n\n // Check if already downloaded\n if (this.stateManager.isDownloaded(statePath, seriesName, episode.number)) {\n return false;\n }\n\n const downloader = downloaderRegistry.getDownloader(episode.url);\n notifier.notify(\n NotificationLevel.HIGHLIGHT,\n `Downloading Episode ${episode.number} of ${seriesName} using ${downloader.getName()}`,\n );\n\n // Calculate filename once (used in both try and catch)\n const paddedNumber = String(episode.number).padStart(2, '0');\n const sanitizedSeriesName = sanitizeFilename(seriesName);\n const filenameWithoutExt = `${sanitizedSeriesName} - ${paddedNumber}`;\n const targetDir = downloadOptions.tempDir || downloadOptions.downloadDir;\n\n try {\n // Clean up any artifacts from previous failed attempts\n await this.cleanupEpisodeArtifacts(targetDir, filenameWithoutExt);\n\n const result = await downloader.download(episode, targetDir, filenameWithoutExt, {\n cookieFile: downloadOptions.cookieFile,\n onProgress: (progress) => notifier.progress(progress),\n onLog: (message) => notifier.notify(NotificationLevel.INFO, message),\n });\n\n // End progress display (add newline)\n notifier.endProgress();\n\n // Verify file exists and has size\n const fileSize = this.verifyDownload(result.filename);\n\n if (fileSize === 0) {\n await this.cleanupFiles(result.allFiles);\n throw new Error('Downloaded file is empty or does not exist');\n }\n\n // Verify duration if required\n if (downloadOptions.minDuration > 0) {\n const fullPath = resolve(result.filename);\n const duration = await VideoValidator.getVideoDuration(fullPath);\n if (duration < downloadOptions.minDuration) {\n // Delete all downloaded files\n await this.cleanupFiles(result.allFiles);\n throw new Error(`Video duration ${duration}s is less than minimum ${downloadOptions.minDuration}s`);\n }\n }\n\n // Move files from tempDir to downloadDir if needed\n if (downloadOptions.tempDir && downloadOptions.tempDir !== downloadOptions.downloadDir) {\n notifier.notify(\n NotificationLevel.INFO,\n `Moving files from temp directory to ${downloadOptions.downloadDir}...`,\n );\n\n // Ensure download directory exists\n await fsPromises.mkdir(downloadOptions.downloadDir, { recursive: true });\n\n for (const file of result.allFiles) {\n try {\n // Resolve 'file' to absolute path just in case\n const absFile = resolve(file);\n\n if (!fs.existsSync(absFile)) {\n notifier.notify(NotificationLevel.WARNING, `File not found, skipping move: ${absFile}`);\n continue;\n }\n\n const fileName = basename(absFile);\n const newPath = join(downloadOptions.downloadDir, fileName);\n await fsPromises.rename(absFile, newPath);\n\n // Update filename if it matches the main file\n if (absFile === resolve(result.filename)) {\n result.filename = newPath;\n }\n } catch (e) {\n notifier.notify(NotificationLevel.ERROR, `Failed to move file ${file}: ${e}`);\n }\n }\n }\n\n // Add to state\n await this.stateManager.addDownloadedEpisode(statePath, seriesName, episode.number);\n\n notifier.notify(\n NotificationLevel.SUCCESS,\n `Downloaded Episode ${episode.number}: ${result.filename} (${this.formatSize(fileSize)})`,\n );\n\n return true;\n } catch (error) {\n // End progress display on error\n notifier.endProgress();\n\n // Clean up any artifacts from this failed attempt\n await this.cleanupEpisodeArtifacts(targetDir, filenameWithoutExt);\n\n const message = `Failed to download Episode ${episode.number}: ${\n error instanceof Error ? error.message : String(error)\n }`;\n\n notifier.notify(NotificationLevel.ERROR, message);\n throw new DownloadError(message, episode.url);\n }\n }\n\n /**\n * Clean up downloaded files\n */\n private async cleanupFiles(files: string[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n for (const file of files) {\n try {\n const fullPath = resolve(file);\n if (fs.existsSync(fullPath)) {\n await fsPromises.unlink(fullPath);\n }\n } catch (e) {\n notifier.notify(NotificationLevel.ERROR, `Failed to delete file ${file}: ${e}`);\n }\n }\n }\n\n /**\n * Clean up all files matching episode pattern (artifacts from failed downloads)\n *\n * @param dir - Directory to clean (tempDir or downloadDir)\n * @param filenameWithoutExt - Episode filename without extension (e.g., \"SeriesName - 01\")\n */\n private async cleanupEpisodeArtifacts(dir: string, filenameWithoutExt: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n try {\n const absDir = resolve(dir);\n if (!fs.existsSync(absDir)) {\n return; // Directory doesn't exist, nothing to clean\n }\n\n const files = await fsPromises.readdir(absDir);\n const pattern = new RegExp(`^${escapeRegExp(filenameWithoutExt)}\\\\..*$`);\n\n let cleanedCount = 0;\n for (const file of files) {\n if (pattern.test(file)) {\n const filePath = join(absDir, file);\n try {\n await fsPromises.unlink(filePath);\n cleanedCount++;\n notifier.notify(NotificationLevel.INFO, `Cleaned up artifact: ${file}`);\n } catch (e) {\n notifier.notify(NotificationLevel.WARNING, `Failed to delete artifact ${file}: ${e}`);\n }\n }\n }\n\n if (cleanedCount > 0) {\n notifier.notify(NotificationLevel.INFO, `Cleaned up ${cleanedCount} artifact(s) for ${filenameWithoutExt}`);\n }\n } catch (e) {\n notifier.notify(NotificationLevel.WARNING, `Failed to cleanup artifacts in ${dir}: ${e}`);\n }\n }\n\n /**\n * Verify downloaded file exists and get its size\n */\n private verifyDownload(filename: string): number {\n const fullPath = resolve(filename);\n\n try {\n const stats = fs.statSync(fullPath);\n return stats.size;\n } catch {\n return 0;\n }\n }\n\n /**\n * Format file size for display\n */\n private formatSize(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let size = bytes;\n let unit = 0;\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024;\n unit++;\n }\n\n return `${size.toFixed(2)} ${units[unit]}`;\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkYtDlpInstalled(): Promise<boolean> {\n return YtDlpDownloader.checkInstalled();\n }\n}\n","/**\n * Utility to sanitize filenames for cross-platform compatibility\n * Specifically targets Windows restrictions which are stricter than *nix\n */\nexport function sanitizeFilename(name: string): string {\n return (\n name\n // Replace Windows illegal characters: < > : \" / \\ | ? *\n .replace(/[<>:\"/\\\\|?*]/g, '_')\n // Remove control characters (0-31 in ASCII)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: Needed to strip control characters\n .replace(/[\\x00-\\x1F]/g, '')\n // Remove trailing spaces and dots (Windows doesn't like them)\n .replace(/[\\s.]+$/, '')\n );\n}\n","import { execa } from 'execa';\nimport { logger } from './logger';\n\n/**\n * Utility to validate video files\n */\n\n/**\n * Get video duration in seconds using ffprobe\n *\n * @param filePath - Path to video file\n * @returns Duration in seconds, or 0 if failed\n */\nexport async function getVideoDuration(filePath: string): Promise<number> {\n try {\n // ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4\n const { stdout } = await execa('ffprobe', [\n '-v',\n 'error',\n '-show_entries',\n 'format=duration',\n '-of',\n 'default=noprint_wrappers=1:nokey=1',\n filePath,\n ]);\n\n const duration = parseFloat(stdout.trim());\n return Number.isNaN(duration) ? 0 : duration;\n } catch (error) {\n logger.error(\n `Failed to get video duration for ${filePath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n return 0;\n }\n}\n\n/**\n * Check if ffprobe is installed\n */\nexport async function checkFfprobeInstalled(): Promise<boolean> {\n try {\n await execa('ffprobe', ['-version']);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Log level\n */\nexport enum LogLevel {\n DEBUG = 'DEBUG',\n INFO = 'INFO',\n SUCCESS = 'SUCCESS',\n WARNING = 'WARNING',\n ERROR = 'ERROR',\n HIGHLIGHT = 'HIGHLIGHT',\n}\n\n/**\n * Logger configuration\n */\nexport type LoggerConfig = {\n level: LogLevel;\n useColors: boolean;\n};\n\n/**\n * ANSI color codes\n */\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n\n // Foreground colors\n black: '\\x1b[30m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n};\n\n/**\n * Logger class with colored console output\n */\nexport class Logger {\n private config: LoggerConfig;\n\n constructor(config: Partial<LoggerConfig> = {}) {\n this.config = {\n level: config.level ?? LogLevel.INFO,\n useColors: config.useColors ?? true,\n };\n }\n\n /**\n * Get emoji for log level\n */\n private getEmoji(level: LogLevel): string {\n switch (level) {\n case LogLevel.DEBUG:\n return '🔍';\n case LogLevel.INFO:\n return 'ℹ️';\n case LogLevel.SUCCESS:\n return '✅';\n case LogLevel.WARNING:\n return '⚠️';\n case LogLevel.ERROR:\n return '❌';\n case LogLevel.HIGHLIGHT:\n return '🌟';\n default:\n return '•';\n }\n }\n\n /**\n * Format date to human readable string (MM-DD HH:mm:ss)\n */\n private formatDate(date: Date): string {\n const month = (date.getMonth() + 1).toString().padStart(2, '0');\n const day = date.getDate().toString().padStart(2, '0');\n const hour = date.getHours().toString().padStart(2, '0');\n const min = date.getMinutes().toString().padStart(2, '0');\n const sec = date.getSeconds().toString().padStart(2, '0');\n return `${month}-${day} ${hour}:${min}:${sec}`;\n }\n\n /**\n * Format log message with timestamp and level\n */\n private format(level: LogLevel, message: string): string {\n const timestamp = this.formatDate(new Date());\n const emoji = this.getEmoji(level);\n return `${timestamp} ${emoji} ${message}`;\n }\n\n /**\n * Apply color to text\n */\n private colorize(text: string, color: string): string {\n if (!this.config.useColors) return text;\n return `${color}${text}${colors.reset}`;\n }\n\n /**\n * Log debug message\n */\n debug(message: string): void {\n if (this.shouldLog(LogLevel.DEBUG)) {\n console.log(this.format(LogLevel.DEBUG, this.colorize(message, colors.dim)));\n }\n }\n\n /**\n * Log info message\n */\n info(message: string): void {\n if (this.shouldLog(LogLevel.INFO)) {\n console.log(this.format(LogLevel.INFO, this.colorize(message, colors.dim + colors.white)));\n }\n }\n\n /**\n * Log success message\n */\n success(message: string): void {\n if (this.shouldLog(LogLevel.SUCCESS)) {\n console.log(this.format(LogLevel.SUCCESS, this.colorize(message, colors.green)));\n }\n }\n\n /**\n * Log warning message\n */\n warning(message: string): void {\n if (this.shouldLog(LogLevel.WARNING)) {\n console.log(this.format(LogLevel.WARNING, this.colorize(message, colors.yellow)));\n }\n }\n\n /**\n * Log error message\n */\n error(message: string): void {\n if (this.shouldLog(LogLevel.ERROR)) {\n console.error(this.format(LogLevel.ERROR, this.colorize(message, colors.red)));\n }\n }\n\n /**\n * Log highlighted message\n */\n highlight(message: string): void {\n if (this.shouldLog(LogLevel.HIGHLIGHT)) {\n console.log(this.format(LogLevel.HIGHLIGHT, this.colorize(message, colors.bright + colors.magenta)));\n }\n }\n\n /**\n * Check if message should be logged based on level\n */\n private shouldLog(level: LogLevel): boolean {\n const levels = [\n LogLevel.DEBUG,\n LogLevel.INFO,\n LogLevel.SUCCESS,\n LogLevel.WARNING,\n LogLevel.ERROR,\n LogLevel.HIGHLIGHT,\n ];\n return levels.indexOf(level) >= levels.indexOf(this.config.level);\n }\n\n /**\n * Set log level\n */\n setLevel(level: LogLevel): void {\n this.config.level = level;\n }\n}\n\n// Default logger instance\nexport const logger: Logger = new Logger();\n","import type { ResolvedConfig } from '../config/config-schema.js';\n\nexport type DownloadOptions = {\n downloadDir: string;\n tempDir?: string;\n cookieFile?: string;\n minDuration: number;\n};\n\nexport function extractDownloadOptions(resolvedConfig: ResolvedConfig<'series'>): DownloadOptions {\n return {\n downloadDir: resolvedConfig.download.downloadDir,\n tempDir: resolvedConfig.download.tempDir,\n cookieFile: resolvedConfig.cookieFile,\n minDuration: resolvedConfig.download.minDuration,\n };\n}\n","import * as fsPromises from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport type { Episode } from '../../types/episode.types';\nimport { BaseDownloader } from '../base-downloader';\nimport type { DownloaderOptions, DownloadResult } from '../types';\n\nexport class YtDlpDownloader extends BaseDownloader {\n getName(): string {\n return 'yt-dlp';\n }\n\n supports(_url: string): boolean {\n return true; // Default downloader supports everything (or tries to)\n }\n\n async download(\n episode: Episode,\n dir: string,\n filenameWithoutExt: string,\n options?: DownloaderOptions,\n ): Promise<DownloadResult> {\n const outputTemplate = join(dir, `${filenameWithoutExt}.%(ext)s`);\n\n // Ensure directory exists\n await fsPromises.mkdir(dir, { recursive: true });\n\n const args = ['--no-warnings', '--newline', '-o', outputTemplate, episode.url];\n\n if (options?.cookieFile) {\n args.unshift('--cookies', options.cookieFile);\n }\n\n let filename: string | null = null;\n const allFiles: Set<string> = new Set();\n const outputBuffer: string[] = [];\n\n try {\n const subprocess = execa('yt-dlp', args, { all: true });\n\n if (subprocess.all) {\n for await (const line of subprocess.all) {\n const text = line.toString().trim();\n if (!text) continue;\n\n // Buffer all output for error debugging\n outputBuffer.push(text);\n\n // Capture filename from \"[download] Destination: ...\" line\n const destMatch = text.match(/\\[download\\] Destination:\\s*(.+)/);\n if (destMatch) {\n filename = destMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n // Capture subtitles from \"[info] Writing video subtitles to: ...\"\n const subMatch = text.match(/\\[info\\] Writing video subtitles to:\\s*(.+)/);\n if (subMatch?.[1]) {\n allFiles.add(subMatch[1]);\n }\n\n // Capture merged file from \"[merge] Merging formats into \"...\"\n const mergeMatch = text.match(/\\[merge\\] Merging formats into \"(.*)\"/);\n if (mergeMatch) {\n filename = mergeMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n // Status messages: [info], [ffmpeg], [merge] - check FIRST\n if (\n text.startsWith('[info]') ||\n text.startsWith('[ffmpeg]') ||\n text.startsWith('[merge]') ||\n text.startsWith('[ExtractAudio]') ||\n text.startsWith('[Metadata]') ||\n text.startsWith('[Thumbnails]')\n ) {\n options?.onLog?.(text);\n continue;\n }\n\n // Progress lines: [download] ...\n if (text.startsWith('[download]')) {\n // Match: [download] 23.8% of ~ 145.41MiB at 563.37KiB/s ETA 03:34 (frag 48/203)\n const progressMatch = text.match(\n /\\[download\\]\\s+(\\d+\\.?\\d*)%\\s+of\\s+~?\\s*([\\d.]+\\w+)\\s+at\\s+~?\\s*([\\d.]+\\w+\\/s)\\s+ETA\\s+(\\S+)/,\n );\n\n if (progressMatch) {\n const [, percentage, totalSize, speed, eta] = progressMatch;\n options?.onProgress?.(`[download] ${percentage}% of ${totalSize} at ${speed} ETA ${eta}`);\n } else {\n // Other download status: Destination, Resuming, etc. - show as log\n options?.onLog?.(text);\n }\n continue;\n }\n\n // Unknown lines - log as info\n options?.onLog?.(text);\n }\n }\n\n await subprocess;\n\n if (!filename) {\n throw new Error('Could not determine downloaded filename from output');\n }\n\n return {\n filename,\n allFiles: Array.from(allFiles),\n };\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n const fullLog = outputBuffer.join('\\n');\n throw new Error(`yt-dlp failed: ${errorMsg}\\n\\nLog output:\\n${fullLog}`);\n }\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkInstalled(): Promise<boolean> {\n try {\n await execa('yt-dlp', ['--version']);\n return true;\n } catch {\n return false;\n }\n }\n}\n","import type { Episode } from '../types/episode.types';\nimport type { Downloader, DownloaderOptions, DownloadResult } from './types';\n\nexport abstract class BaseDownloader implements Downloader {\n abstract getName(): string;\n abstract supports(url: string): boolean;\n abstract download(\n episode: Episode,\n dir: string,\n filenameWithoutExt: string,\n options?: DownloaderOptions,\n ): Promise<DownloadResult>;\n}\n","import { YtDlpDownloader } from './impl/yt-dlp-downloader';\nimport type { Downloader } from './types';\n\nexport class DownloaderRegistry {\n private downloaders: Downloader[] = [];\n private defaultDownloader: Downloader;\n\n constructor() {\n this.defaultDownloader = new YtDlpDownloader();\n }\n\n register(downloader: Downloader): void {\n this.downloaders.push(downloader);\n }\n\n getDownloader(url: string): Downloader {\n // Find first specific downloader that supports the URL\n // Since default downloader returns true for everything, we check it last\n // But here we iterate registered custom downloaders first\n for (const downloader of this.downloaders) {\n if (downloader.supports(url)) {\n return downloader;\n }\n }\n\n return this.defaultDownloader;\n }\n}\n\nexport const downloaderRegistry = new DownloaderRegistry();\n","import { HandlerError } from '../errors/custom-errors';\nimport type { DomainHandler, HandlerRegistry } from '../types/handler.types';\nimport { extractDomain } from '../utils/url-utils';\n\n/**\n * Handler registry implementation\n */\nexport class Registry implements HandlerRegistry {\n private handlers: Map<string, DomainHandler> = new Map();\n\n /**\n * Register a handler\n */\n register(handler: DomainHandler): void {\n this.handlers.set(handler.getDomain(), handler);\n }\n\n /**\n * Get handler for URL\n */\n getHandler(url: string): DomainHandler | undefined {\n const domain = extractDomain(url);\n\n // First try exact match\n if (this.handlers.has(domain)) {\n return this.handlers.get(domain);\n }\n\n // Then try subdomain match (e.g., www.wetv.vip -> wetv.vip)\n for (const [handlerDomain, handler] of this.handlers.entries()) {\n if (domain === handlerDomain || domain.endsWith(`.${handlerDomain}`) || handlerDomain.endsWith(`.${domain}`)) {\n return handler;\n }\n }\n\n return undefined;\n }\n\n /**\n * Get all registered domains\n */\n getDomains(): string[] {\n return Array.from(this.handlers.keys());\n }\n\n /**\n * Get handler or throw error\n */\n getHandlerOrThrow(url: string): DomainHandler {\n const handler = this.getHandler(url);\n if (!handler) {\n throw new HandlerError(\n `No handler found for domain: \"${extractDomain(url)}\". ` + `Supported domains: ${this.getDomains().join(', ')}`,\n url,\n );\n }\n return handler;\n }\n}\n\n// Global registry instance\nexport const handlerRegistry: Registry = new Registry();\n","import * as cheerio from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport { HandlerError } from '../../errors/custom-errors';\nimport type { Episode, EpisodeType } from '../../types/episode.types';\nimport type { DomainHandler } from '../../types/handler.types';\nimport { extractDomain } from '../../utils/url-utils';\n\n/**\n * Base handler class with common functionality\n */\nexport abstract class BaseHandler implements DomainHandler {\n abstract getDomain(): string;\n\n abstract extractEpisodes(url: string, cookies?: string): Promise<Episode[]>;\n\n /**\n * Check if handler supports the given URL\n */\n supports(url: string): boolean {\n try {\n const domain = extractDomain(url);\n return domain === this.getDomain() || domain.endsWith(`.${this.getDomain()}`);\n } catch {\n return false;\n }\n }\n\n /**\n * Fetch HTML from URL with optional cookies\n */\n protected async fetchHtml(url: string, cookies?: string): Promise<string> {\n const headers: Record<string, string> = {\n 'User-Agent':\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n 'Accept-Language': 'en-US,en;q=0.9',\n };\n\n if (cookies) {\n headers.Cookie = cookies;\n }\n\n try {\n const response = await fetch(url, { headers });\n\n if (!response.ok) {\n throw new HandlerError(`HTTP ${response.status}: ${response.statusText}`, url);\n }\n\n return await response.text();\n } catch (error) {\n if (error instanceof HandlerError) {\n throw error;\n }\n throw new HandlerError(`Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`, url);\n }\n }\n\n /**\n * Parse cheerio document from HTML\n */\n protected parseHtml(html: string): cheerio.CheerioAPI {\n return cheerio.load(html);\n }\n\n /**\n * Parse episode number from text\n * Handles formats like \"第1集\", \"EP1\", \"Episode 1\", etc.\n */\n protected parseEpisodeNumber(text: string): number | null {\n // Chinese format: 第X集\n const chineseMatch = text.match(/第(\\d+)集/);\n if (chineseMatch?.[1]) {\n return parseInt(chineseMatch[1], 10);\n }\n\n // EP prefix: EP1, ep01, etc.\n const epMatch = text.match(/ep\\s?(\\d+)/i);\n if (epMatch?.[1]) {\n return parseInt(epMatch[1], 10);\n }\n\n // Episode prefix: Episode 1, E1, etc.\n const episodeMatch = text.match(/(?:episode|e)\\s?(\\d+)/i);\n if (episodeMatch?.[1]) {\n return parseInt(episodeMatch[1], 10);\n }\n\n // Standalone number\n const numberMatch = text.match(/\\b(\\d+)\\b/);\n if (numberMatch?.[1]) {\n return parseInt(numberMatch[1], 10);\n }\n\n return null;\n }\n\n /**\n * Parse episode type from class names or text\n */\n protected parseEpisodeType(element: AnyNode, $: cheerio.CheerioAPI): EpisodeType {\n const $el = $(element);\n const className = $el.attr('class') || '';\n const text = $el.text().toLowerCase();\n\n // Check for VIP indicators\n if (className.includes('vip') || text.includes('vip') || text.includes('会员')) {\n return 'vip' as EpisodeType;\n }\n\n // Check for preview indicators\n if (\n className.includes('preview') ||\n className.includes('trailer') ||\n text.includes('preview') ||\n text.includes('预告')\n ) {\n return 'preview' as EpisodeType;\n }\n\n // Check for locked indicators\n if (\n className.includes('locked') ||\n className.includes('lock') ||\n text.includes('locked') ||\n text.includes('锁定')\n ) {\n return 'locked' as EpisodeType;\n }\n\n return 'available' as EpisodeType;\n }\n}\n","import type { CheerioAPI } from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype NextData = {\n props?: {\n pageProps?: {\n data?: string;\n };\n };\n};\n\ntype PageData = {\n albumInfo?: {\n albumId: string;\n title: string;\n };\n videoList?: Array<{\n vid: string;\n episode: string;\n title: string;\n isTrailer?: number | boolean;\n payStatus?: number;\n }>;\n};\n\n/**\n * Handler for iq.com domain (iQIYI international)\n */\nexport class IQiyiHandler extends BaseHandler {\n getDomain(): string {\n return 'iq.com';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n\n // Try to extract from __NEXT_DATA__ first (includes all episodes)\n const nextDataEpisodes = this.extractFromNextData(html);\n if (nextDataEpisodes.length > 0) {\n return nextDataEpisodes;\n }\n\n // Fallback to HTML parsing (old method)\n return this.extractFromHtml(html);\n }\n\n /**\n * Extract episodes from __NEXT_DATA__ JSON embedded in HTML\n * This method gets ALL episodes including those not visible due to pagination\n */\n private extractFromNextData(html: string): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n // Extract __NEXT_DATA__ script content\n const match = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>(.+?)<\\/script>/s);\n if (!match || !match[1]) {\n return episodes;\n }\n\n const nextData: NextData = JSON.parse(match[1]);\n const dataStr = nextData.props?.pageProps?.data;\n if (!dataStr) return episodes;\n\n const pageData: PageData = JSON.parse(dataStr as string);\n\n const { albumInfo, videoList = [] } = pageData;\n const { albumId, title } = albumInfo || {};\n\n // Process each video\n for (const video of videoList) {\n const { vid, episode, isTrailer, payStatus } = video;\n\n // Skip trailers\n if (isTrailer) {\n continue;\n }\n\n const episodeNumber = this.parseEpisodeNumber(episode);\n if (!episodeNumber) {\n continue;\n }\n\n // Build URL: /play/{albumId}-{vid}?lang=en_us\n const episodeUrl = `https://www.iq.com/play/${albumId}-${vid}?lang=en_us`;\n\n // Determine episode type based on payStatus\n const type = this.determineTypeFromPayStatus(payStatus);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: `${title} - Episode ${episode}`,\n extractedAt: new Date(),\n });\n }\n } catch (error) {\n // If extraction fails, return empty array to trigger fallback\n console.error('Failed to extract from __NEXT_DATA__:', error);\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Extract episodes from HTML (fallback method)\n */\n private extractFromHtml(html: string): Episode[] {\n const $ = this.parseHtml(html);\n const episodes: Episode[] = [];\n\n // Try multiple selectors for episode lists\n const selectors = [\n 'ul li a[href*=\"/play/\"]', // Most common pattern\n '.album-episode-item a[href*=\"/play/\"]',\n '.episode-item a[href*=\"/play/\"]',\n '.intl-play-item a[href*=\"/play/\"]',\n '[data-episode] a[href*=\"/play/\"]',\n ];\n\n for (const selector of selectors) {\n const links = $(selector);\n\n if (links.length > 0) {\n links.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n\n // If we found episodes with this selector, break\n if (episodes.length > 0) {\n break;\n }\n }\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Determine episode type from payStatus\n */\n private determineTypeFromPayStatus(payStatus?: number): EpisodeType {\n if (payStatus === 6) {\n return EpisodeType.VIP;\n }\n return EpisodeType.AVAILABLE;\n }\n\n /**\n * Process a single episode link element\n */\n private processEpisodeLink($: CheerioAPI, element: AnyNode, episodes: Episode[]): void {\n const $el = $(element);\n const href = $el.attr('href');\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://www.iq.com${href}`;\n\n // Get text content for parsing\n const text = $el.text().trim();\n const title = $el.attr('title') || undefined;\n\n // Filter out BTS episodes (behind-the-scenes) - check text content FIRST\n // This must happen before extracting episode number from URL\n if (text.toUpperCase().includes('BTS')) {\n return;\n }\n\n // Extract episode number from text FIRST (more reliable than URL)\n let episodeNumber = this.parseEpisodeNumber(text);\n\n // If not found in text, try href (fallback)\n if (!episodeNumber) {\n episodeNumber = this.parseEpisodeNumber(href);\n }\n\n if (!episodeNumber) return;\n\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (exists) return;\n\n // Determine episode type based on VIP badges\n const type = this.determineEpisodeType($, element);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title,\n extractedAt: new Date(),\n });\n }\n\n /**\n * Determine episode type based on badges (VIP, etc.)\n */\n private determineEpisodeType($: CheerioAPI, element: AnyNode): EpisodeType {\n // Check parent elements for VIP badge\n const $parent = $(element).closest('li, div');\n\n if ($parent.length) {\n const parentText = $parent.text() || '';\n\n // Check for VIP badge\n if (parentText.toUpperCase().includes('VIP')) {\n return EpisodeType.VIP;\n }\n }\n\n // Default: available (free episodes)\n return EpisodeType.AVAILABLE;\n }\n}\n","import type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype MGTVResponse = {\n code: number;\n msg: string;\n data: {\n total_page: number;\n current_page: number;\n list: Array<{\n t1: string; // Episode number (e.g. \"1\")\n t2: string; // Title (e.g. \"EP 1\")\n t4: string; // Chinese title (e.g. \"第1集\")\n isvip: string; // \"1\" = VIP, \"0\" = Free\n url: string; // Relative URL (e.g. \"/b/823701/23967831.html\")\n video_id: string;\n clip_id: string;\n time: string; // Duration (e.g. \"45:00\")\n ts: string; // Timestamp\n }>;\n info: {\n title: string;\n isvip: string;\n };\n };\n};\n\n/**\n * Handler for mgtv.com domain\n */\nexport class MGTVHandler extends BaseHandler {\n getDomain(): string {\n return 'mgtv.com';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const videoId = this.extractVideoId(url);\n if (!videoId) {\n throw new Error('Could not extract video ID from URL');\n }\n\n const episodes: Episode[] = [];\n let page = 0;\n let totalPages = 1;\n\n do {\n const apiUrl = `https://tinker.glb.mgtv.com/episode/list?src=intelmgtv&abroad=10&_support=10000000&version=5.5.35&video_id=${videoId}&page=${page}&size=50&platform=4`;\n\n const response = await this.fetchHtml(apiUrl, cookies);\n let data: MGTVResponse;\n\n try {\n data = JSON.parse(response);\n } catch (_e) {\n throw new Error('Failed to parse MGTV API response');\n }\n\n if (data.code !== 200) {\n throw new Error(`MGTV API error: ${data.msg}`);\n }\n\n totalPages = data.data.total_page;\n\n for (const item of data.data.list) {\n const episodeNumber = this.parseEpisodeNumber(item.t1);\n if (!episodeNumber) continue;\n\n const episodeUrl = `https://w.mgtv.com${item.url}`;\n\n episodes.push({\n number: episodeNumber,\n title: item.t2 || item.t4 || `Episode ${episodeNumber}`,\n url: episodeUrl,\n type: item.isvip === '1' ? EpisodeType.VIP : EpisodeType.AVAILABLE,\n extractedAt: new Date(),\n });\n }\n\n page++;\n } while (page < totalPages);\n\n // Deduplicate and sort\n return this.deduplicateEpisodes(episodes);\n }\n\n private extractVideoId(url: string): string | null {\n // Matches /b/823701/23967831.html -> 23967831\n const match = url.match(/\\/b\\/\\d+\\/(\\d+)\\.html/);\n return match ? match[1] || null : null;\n }\n\n /**\n * Deduplicate episodes based on episode number\n */\n private deduplicateEpisodes(episodes: Episode[]): Episode[] {\n const uniqueEpisodes = new Map<number, Episode>();\n\n for (const episode of episodes) {\n if (!uniqueEpisodes.has(episode.number)) {\n uniqueEpisodes.set(episode.number, episode);\n }\n }\n\n return Array.from(uniqueEpisodes.values()).sort((a, b) => a.number - b.number);\n }\n}\n","import type { CheerioAPI } from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype NextData = {\n props?: {\n pageProps?: {\n data?: string;\n };\n };\n};\n\ntype PageData = {\n canPlay: boolean;\n coverInfo: {\n cid: string;\n title: string;\n secondTitle?: string;\n };\n videoList: Array<{\n vid: string;\n episode: string;\n isTrailer: number | boolean;\n payStatus?: number;\n defaultPayStatus?: number;\n coverList?: string[];\n labels?: {\n [key: string]: {\n text: string;\n color?: string;\n };\n };\n }>;\n};\n\n/**\n * Handler for wetv.vip domain\n */\nexport class WeTVHandler extends BaseHandler {\n getDomain(): string {\n return 'wetv.vip';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n\n // Try to extract from __NEXT_DATA__ first (includes all episodes)\n const nextDataEpisodes = this.extractFromNextData(html);\n if (nextDataEpisodes.length > 0) {\n return nextDataEpisodes;\n }\n\n // Fallback to HTML parsing (old method)\n return this.extractFromHtml(html);\n }\n\n /**\n * Extract episodes from __NEXT_DATA__ JSON embedded in HTML\n * This method gets ALL episodes including those not visible due to pagination\n */\n private extractFromNextData(html: string): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n // Extract __NEXT_DATA__ script content\n const match = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>(.+?)<\\/script>/s);\n if (!match || !match[1]) {\n return episodes;\n }\n\n const nextData: NextData = JSON.parse(match[1]);\n const dataStr = nextData.props?.pageProps?.data;\n if (!dataStr) return episodes;\n const pageData: PageData = JSON.parse(dataStr as string);\n\n const { coverInfo, videoList = [] } = pageData;\n const { cid, title } = coverInfo;\n\n // Extract CID from first video's coverList if not available in coverInfo\n const coverId = videoList[0]?.coverList?.[0] || cid;\n\n // Process each video\n for (const video of videoList) {\n const { vid, episode, isTrailer } = video;\n\n // Skip trailers/teasers\n if (isTrailer) {\n continue;\n }\n\n const episodeNumber = this.parseEpisodeNumber(episode);\n if (!episodeNumber) {\n continue;\n }\n\n // Build URL: /en/play/{cid}/{vid}-EP{episode}%3A{title}\n const encodedTitle = encodeURIComponent(title);\n const episodeUrl = `https://wetv.vip/en/play/${coverId}/${vid}-EP${episode}%3A${encodedTitle}`;\n\n // Determine episode type based on labels and payStatus\n const type = this.determineTypeFromVideo(video);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: `${title} - Episode ${episode}`,\n extractedAt: new Date(),\n });\n }\n } catch (error) {\n // If extraction fails, return empty array to trigger fallback\n console.error('Failed to extract from __NEXT_DATA__:', error);\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Extract episodes from HTML (fallback method)\n * This only gets visible episodes (first 30 due to pagination)\n */\n private extractFromHtml(html: string): Episode[] {\n const $ = this.parseHtml(html);\n const episodes: Episode[] = [];\n\n // WeTV uses play-video__link class for episode links\n const episodeLinks = $('a.play-video__link[href*=\"/play/\"][href*=\"EP\"]');\n\n if (episodeLinks.length === 0) {\n // Fallback: try generic selector\n const fallbackLinks = $('a[href*=\"/play/\"]').filter((_, el) => {\n const href = $(el).attr('href') || '';\n return href.includes('EP');\n });\n\n fallbackLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n } else {\n episodeLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Determine episode type from video data\n * Checks labels first, then falls back to payStatus\n */\n private determineTypeFromVideo(video: PageData['videoList'][0]): EpisodeType {\n const { labels, payStatus, defaultPayStatus } = video;\n\n // Check labels first (more reliable)\n if (labels) {\n for (const key in labels) {\n const label = labels[key];\n if (!label) continue;\n const labelText = label.text?.toLowerCase() || '';\n\n if (labelText === 'express') {\n return EpisodeType.EXPRESS;\n }\n if (labelText === 'teaser') {\n return EpisodeType.TEASER;\n }\n if (labelText === 'vip') {\n return EpisodeType.VIP;\n }\n }\n }\n\n // Fallback to payStatus\n const status = payStatus || defaultPayStatus;\n if (status === 6) {\n return EpisodeType.VIP;\n }\n if (status === 12) {\n return EpisodeType.EXPRESS;\n }\n return EpisodeType.AVAILABLE;\n }\n\n /**\n * Process a single episode link element\n */\n private processEpisodeLink($: CheerioAPI, element: AnyNode, episodes: Episode[]): void {\n const $el = $(element);\n const href = $el.attr('href');\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://wetv.vip${href}`;\n\n // Extract episode number from aria-label (e.g., \"Play episode 01\")\n const ariaLabel = $el.attr('aria-label') || '';\n const episodeNumber = this.parseEpisodeNumber(ariaLabel);\n\n if (!episodeNumber) return;\n\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (exists) return;\n\n // Determine episode type based on badges\n const type = this.determineEpisodeType($, element);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: $el.attr('title') || undefined,\n extractedAt: new Date(),\n });\n }\n\n /**\n * Determine episode type based on badges (VIP, Teaser, Express)\n */\n private determineEpisodeType($: CheerioAPI, element: AnyNode): EpisodeType {\n // Check for badges in parent li or sibling elements\n const $li = $(element).closest('li');\n\n if ($li.length) {\n // Look for span.play-video__label\n const badge = $li.find('span.play-video__label').first();\n\n if (badge.length) {\n const badgeText = badge.text().trim().toLowerCase();\n\n // Check badge types\n if (badgeText === 'vip' || badgeText.includes('vip')) {\n return EpisodeType.VIP;\n }\n if (badgeText === 'teaser' || badgeText.includes('teaser')) {\n return EpisodeType.TEASER;\n }\n if (badgeText === 'express' || badgeText.includes('express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Also check text content for badges\n const liText = $li.text() || '';\n if (liText.includes('VIP') && !liText.includes('Teaser')) {\n return EpisodeType.VIP;\n }\n if (liText.includes('Teaser')) {\n return EpisodeType.TEASER;\n }\n if (liText.includes('Express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Default: available (free episodes)\n return EpisodeType.AVAILABLE;\n }\n}\n","import { logger } from '../utils/logger';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Console notifier for terminal output\n */\nexport class ConsoleNotifier implements Notifier {\n private lastProgressLength = 0;\n\n notify(level: NotificationLevel, message: string): void {\n // If there was an active progress line, clear it first so the log appears cleanly\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n this.lastProgressLength = 0;\n }\n\n switch (level) {\n case NotificationLevel.INFO:\n logger.info(message);\n break;\n case NotificationLevel.SUCCESS:\n logger.success(message);\n break;\n case NotificationLevel.WARNING:\n logger.warning(message);\n break;\n case NotificationLevel.ERROR:\n logger.error(message);\n break;\n case NotificationLevel.HIGHLIGHT:\n logger.highlight(message);\n break;\n }\n }\n\n progress(message: string): void {\n // Clear previous progress by overwriting with spaces\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n }\n\n // Write new progress message\n process.stdout.write(`\\r${message}`);\n this.lastProgressLength = message.length;\n }\n\n /**\n * Finalize progress (add newline after last progress update)\n */\n endProgress(): void {\n if (this.lastProgressLength > 0) {\n process.stdout.write('\\n');\n this.lastProgressLength = 0;\n }\n }\n}\n","import { NotificationError } from '../errors/custom-errors';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Telegram configuration\n */\nexport type TelegramConfig = {\n botToken: string;\n chatId: string;\n};\n\n/**\n * Telegram notifier for error notifications only\n */\nexport class TelegramNotifier implements Notifier {\n private config: TelegramConfig;\n private apiUrl: string;\n\n constructor(config: TelegramConfig) {\n this.config = config;\n this.apiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;\n }\n\n /**\n * Send notification via Telegram\n * Only sends ERROR level notifications\n */\n async notify(level: NotificationLevel, message: string): Promise<void> {\n // Only send error notifications\n if (level !== NotificationLevel.ERROR) {\n return;\n }\n\n try {\n const emoji = this.getEmoji(level);\n // Truncate message if it's too long (Telegram limit is 4096 chars)\n // We reserve ~100 chars for header and tags\n const MAX_LENGTH = 4000;\n let safeMessage = message;\n if (safeMessage.length > MAX_LENGTH) {\n safeMessage = `${safeMessage.substring(0, MAX_LENGTH)}\\n... (truncated)`;\n }\n\n // Escape HTML characters in the message to prevent parsing errors\n const escapedMessage = this.escapeHtml(safeMessage);\n // Use <pre> tag for the error message to preserve formatting and monospacing\n const formattedMessage = `${emoji} <b>wetvlo Error</b>\\n\\n<pre>${escapedMessage}</pre>`;\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n chat_id: this.config.chatId,\n text: formattedMessage,\n parse_mode: 'HTML',\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new NotificationError(\n `Failed to send Telegram notification: ${response.status} ${response.statusText}\\n${errorText}`,\n );\n }\n } catch (error) {\n // Don't throw for notification errors, just log them\n console.error('Telegram notification failed:', error);\n }\n }\n\n /**\n * Escape HTML special characters\n */\n private escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n }\n\n /**\n * Get emoji for notification level\n */\n private getEmoji(level: NotificationLevel): string {\n switch (level) {\n case NotificationLevel.INFO:\n return 'ℹ️';\n case NotificationLevel.SUCCESS:\n return '✅';\n case NotificationLevel.WARNING:\n return '⚠️';\n case NotificationLevel.ERROR:\n return '❌';\n case NotificationLevel.HIGHLIGHT:\n return '🔔';\n default:\n return '';\n }\n }\n\n /**\n * Progress updates are not sent to Telegram (no-op)\n */\n async progress(_message: string): Promise<void> {\n // Telegram doesn't need real-time progress updates\n }\n\n /**\n * Progress finalization is no-op for Telegram\n */\n async endProgress(): Promise<void> {\n // No-op for Telegram\n }\n}\n","/**\n * QueueManager - Orchestrates check and download queues with UniversalScheduler\n *\n * Manages the queue-based architecture with:\n * - Per-domain check and download queues\n * - Universal scheduler for single-task execution globally\n * - Event-driven scheduling (no polling loops)\n * - Graceful shutdown\n * - Proper end-to-start cooldowns\n */\n\nimport { createHash } from 'node:crypto';\nimport { AppContext } from '../app-context.js';\nimport type { ResolvedConfig } from '../config/config-schema.js';\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { handlerRegistry } from '../handlers/handler-registry.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport type { StateManager } from '../state/state-manager.js';\nimport type { Episode, EpisodeType } from '../types/episode.types.js';\nimport { extractDomain } from '../utils/url-utils.js';\nimport type { CheckQueueItem, DownloadQueueItem } from './types.js';\nimport { UniversalScheduler } from './universal-scheduler.js';\n\n/**\n * Queue Manager for orchestrating all queues with universal scheduler\n */\nexport class QueueManager {\n private stateManager: StateManager;\n private downloadManager: DownloadManager;\n\n // Universal scheduler (handles all check and download queues)\n private scheduler: UniversalScheduler<CheckQueueItem | DownloadQueueItem>;\n\n // Running state\n private running = false;\n\n // Domain handlers cache\n private domainHandlers = new Map<string, ReturnType<typeof handlerRegistry.getHandlerOrThrow>>();\n\n /**\n * Create a new QueueManager\n *\n * @param downloadManager - Download manager instance\n * @param schedulerFactory - Optional factory for creating scheduler (for testing)\n */\n constructor(\n downloadManager: DownloadManager,\n schedulerFactory?: (\n executor: (task: CheckQueueItem | DownloadQueueItem, queueName: string) => Promise<void>,\n ) => UniversalScheduler<CheckQueueItem | DownloadQueueItem>,\n ) {\n // Get StateManager from AppContext\n this.stateManager = AppContext.getStateManager();\n this.downloadManager = downloadManager;\n\n // Create universal scheduler with executor callback\n const createScheduler = schedulerFactory || ((executor) => new UniversalScheduler(executor));\n this.scheduler = createScheduler(async (task, queueName) => {\n await this.executeTask(task, queueName);\n });\n\n // Set up wait notification\n this.scheduler.setOnWait((queueName, waitMs) => {\n const notifier = AppContext.getNotifier();\n const seconds = Math.round(waitMs / 1000);\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (type === 'download') {\n notifier.notify(NotificationLevel.INFO, `[${domain}] Next download in ${seconds}s...`);\n } else if (type === 'check') {\n notifier.notify(NotificationLevel.INFO, `[${domain}] Next check in ${seconds}s...`);\n }\n });\n }\n\n /**\n * Add a series to the check queue\n *\n * @param seriesUrl - Series URL\n */\n addSeriesCheck(seriesUrl: string): void {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const domain = extractDomain(seriesUrl);\n\n // Get series name from resolved config for notification\n const config = registry.resolve(seriesUrl, 'series');\n const seriesName = config.name;\n\n // Register download queue for this domain (shared across series)\n this.registerDownloadQueue(domain);\n\n // Register specific check queue for this series (isolated interval)\n const queueName = this.registerSeriesCheckQueue(domain, seriesUrl);\n\n // Add series to check queue\n const item: CheckQueueItem = {\n seriesUrl,\n attemptNumber: 1,\n retryCount: 0,\n };\n\n this.scheduler.addTask(queueName, item);\n\n notifier.notify(NotificationLevel.INFO, `[QueueManager] Added ${seriesName} to check queue for domain ${domain}`);\n }\n\n /**\n * Add episodes to the download queue\n *\n * @param seriesUrl - Series URL\n * @param episodes - Episodes to download\n */\n addEpisodes(seriesUrl: string, episodes: Episode[]): void {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n\n if (episodes.length === 0) {\n return;\n }\n\n // Get series name from resolved config for notification\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const domain = extractDomain(seriesUrl);\n\n // Register download queues for this domain if not already registered\n this.registerDownloadQueue(domain);\n\n // Get download delay from resolved config\n const { downloadDelay } = resolvedConfig.download;\n\n // Add episodes to download queue with staggered delays\n for (let i = 0; i < episodes.length; i++) {\n const episode = episodes[i];\n if (!episode) continue;\n\n const item: DownloadQueueItem = {\n seriesUrl,\n episode,\n retryCount: 0,\n };\n\n const queueName = `download:${domain}`;\n // Stagger episodes by downloadDelay\n const delayMs = i * downloadDelay * 1000;\n this.scheduler.addTask(queueName, item, delayMs);\n }\n\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[QueueManager] Added ${episodes.length} episodes to download queue for ${seriesName} (domain ${domain})`,\n );\n }\n\n /**\n * Update configuration\n */\n updateConfig(): void {\n const notifier = AppContext.getNotifier();\n // Config is reloaded in AppContext, we just need to notify\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Configuration will be reloaded from AppContext');\n }\n\n /**\n * Start all queues\n */\n start(): void {\n const notifier = AppContext.getNotifier();\n\n if (this.running) {\n throw new Error('QueueManager is already running');\n }\n\n this.running = true;\n this.scheduler.resume();\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Started queue processing');\n }\n\n /**\n * Stop all queues gracefully\n *\n * Waits for current task to complete.\n */\n async stop(): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n if (!this.running) {\n return;\n }\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Stopping queue processing...');\n\n this.scheduler.stop();\n this.running = false;\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Queue processing stopped');\n }\n\n /**\n * Check if there is active processing or pending tasks\n *\n * @returns Whether scheduler is actively processing or has pending tasks\n */\n hasActiveProcessing(): boolean {\n return this.scheduler.isExecutorBusy() || this.scheduler.hasPendingTasks();\n }\n\n /**\n * Get queue statistics\n *\n * @returns Object with queue statistics\n */\n getQueueStats(): {\n checkQueues: Record<string, { length: number; processing: boolean }>;\n downloadQueues: Record<string, { length: number; processing: boolean }>;\n } {\n const stats = this.scheduler.getStats();\n const checkQueues: Record<string, { length: number; processing: boolean }> = {};\n const downloadQueues: Record<string, { length: number; processing: boolean }> = {};\n\n for (const [queueName, queueStats] of stats.entries()) {\n if (queueName.startsWith('check:')) {\n const parts = queueName.split(':');\n const domain = parts[1]; // Extract domain from check:domain:hash\n if (!domain) continue;\n\n if (!checkQueues[domain]) {\n checkQueues[domain] = { length: 0, processing: false };\n }\n\n checkQueues[domain].length += queueStats.queueLength;\n if (queueStats.isExecuting) {\n checkQueues[domain].processing = true;\n }\n } else if (queueName.startsWith('download:')) {\n const domain = queueName.slice(9); // Remove \"download:\" prefix\n downloadQueues[domain] = {\n length: queueStats.queueLength,\n processing: queueStats.isExecuting,\n };\n }\n }\n\n return { checkQueues, downloadQueues };\n }\n\n /**\n * Register download queue for a domain (shared across series)\n */\n private registerDownloadQueue(domain: string): void {\n const registry = AppContext.getConfig();\n const queueName = `download:${domain}`;\n\n // Check if queue is already registered\n if (this.scheduler.hasQueue(queueName)) {\n return;\n }\n\n // Resolve configuration - use any URL from this domain to get domain-level config\n const testUrl = `https://${domain}/`;\n const resolvedConfig = registry.resolve(testUrl, 'domain');\n const { downloadDelay } = resolvedConfig.download;\n\n // Register queue with scheduler\n this.scheduler.registerQueue(queueName, downloadDelay * 1000); // Convert to ms\n }\n\n /**\n * Register specific check queue for a series (isolated interval)\n */\n private registerSeriesCheckQueue(domain: string, seriesUrl: string): string {\n const registry = AppContext.getConfig();\n\n // Generate a short hash of the URL to ensure uniqueness and safe queue name\n const hash = createHash('md5').update(seriesUrl).digest('hex').substring(0, 12);\n const queueName = `check:${domain}:${hash}`;\n\n // Check if queue is already registered\n if (this.scheduler.hasQueue(queueName)) {\n return queueName;\n }\n\n // Resolve configuration\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const { checkInterval } = resolvedConfig.check;\n\n // Register queue with scheduler\n this.scheduler.registerQueue(queueName, checkInterval * 1000); // Convert to ms\n\n // Ensure we have a handler for this domain\n if (!this.domainHandlers.has(domain)) {\n const handler = handlerRegistry.getHandlerOrThrow(`https://${domain}/`);\n this.domainHandlers.set(domain, handler);\n }\n\n return queueName;\n }\n\n /**\n * Execute a task from the scheduler\n *\n * This is the executor callback that handles both check and download tasks.\n *\n * @param task - Task to execute\n * @param queueName - Queue name (format: \"check:domain\" or \"download:domain\")\n */\n private async executeTask(task: CheckQueueItem | DownloadQueueItem, queueName: string): Promise<void> {\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (!type || !domain) {\n throw new Error(`Invalid queue name format: ${queueName}`);\n }\n\n if (type === 'check') {\n await this.executeCheck(task as CheckQueueItem, domain, queueName);\n } else if (type === 'download') {\n await this.executeDownload(task as DownloadQueueItem, domain, queueName);\n } else {\n throw new Error(`Unknown queue type: ${type}`);\n }\n }\n\n /**\n * Execute a check task\n *\n * @param item - Check queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeCheck(item: CheckQueueItem, domain: string, queueName: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const { seriesUrl, attemptNumber, retryCount = 0 } = item;\n\n // Get handler for this domain\n const handler = this.domainHandlers.get(domain);\n if (!handler) {\n throw new Error(`No handler found for domain ${domain}`);\n }\n\n // Get settings\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const { count: checksCount, checkInterval } = resolvedConfig.check;\n\n try {\n // Perform the check\n const result = await this.performCheck(handler, seriesUrl, resolvedConfig, attemptNumber, domain);\n\n if (result.hasNewEpisodes) {\n // Episodes found - send to download queue, do NOT requeue\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Found ${result.episodes.length} new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Add episodes to download queue\n this.addEpisodes(seriesUrl, result.episodes);\n\n // Session complete - do not requeue\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // No episodes found - check if we should requeue\n if (attemptNumber < checksCount) {\n // Requeue with interval delay\n const intervalMs = checkInterval * 1000;\n const requeueDelay = result.requeueDelay ?? intervalMs;\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] No new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount}), requeueing in ${Math.round(requeueDelay / 1000)}s`,\n );\n\n // Requeue with incremented attempt number\n const requeuedItem: CheckQueueItem = {\n seriesUrl,\n attemptNumber: attemptNumber + 1,\n retryCount: 0,\n };\n\n this.scheduler.addTask(queueName, requeuedItem, requeueDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Checks exhausted - do not requeue\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checks exhausted for ${seriesName} (${checksCount} attempts with no new episodes)`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Get download settings for retry config\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with exponential backoff (convert seconds to ms)\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000,\n backoffMultiplier,\n jitterPercentage,\n );\n\n notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Check failed for ${seriesName}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count (same attempt number)\n const requeuedItem: CheckQueueItem = {\n seriesUrl,\n attemptNumber,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Max retries exceeded - log error and give up\n notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to check ${seriesName} after ${retryCount} retry attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n }\n\n /**\n * Execute a download task\n *\n * @param item - Download queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeDownload(item: DownloadQueueItem, domain: string, queueName: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const { seriesUrl, episode, retryCount = 0 } = item;\n\n // Resolve config\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const { downloadDelay } = resolvedConfig.download;\n\n try {\n // Attempt download\n await this.downloadManager.download(seriesUrl, episode);\n\n // Success - log and continue\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Successfully queued download of Episode ${episode.number} for ${seriesName}`,\n );\n\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Check if we should retry\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with backoff\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000, // convert seconds to ms\n backoffMultiplier,\n jitterPercentage,\n );\n\n notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Download failed for Episode ${episode.number}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count\n const requeuedItem: DownloadQueueItem = {\n seriesUrl,\n episode,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } else {\n // Max retries exceeded - log error and give up\n notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to download Episode ${episode.number} after ${retryCount + 1} attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n }\n }\n }\n\n /**\n * Perform the actual check for new episodes\n *\n * @param handler - Domain handler\n * @param seriesUrl - Series URL\n * @param config - Resolved series configuration\n * @param attemptNumber - Current attempt number\n * @param domain - Domain name\n * @returns Check result\n */\n private async performCheck(\n handler: ReturnType<typeof handlerRegistry.getHandlerOrThrow>,\n seriesUrl: string,\n config: ResolvedConfig<'series'>,\n attemptNumber: number,\n domain: string,\n ): Promise<{ hasNewEpisodes: boolean; episodes: Episode[]; requeueDelay?: number }> {\n const notifier = AppContext.getNotifier();\n const seriesName = config.name;\n const checksCount = config.check.count;\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checking ${seriesUrl} for new episodes... (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Extract episodes from the series page\n const episodes = await handler.extractEpisodes(seriesUrl);\n\n // Log episodes by type\n const episodesByType = new Map<EpisodeType, number>();\n episodes.forEach((ep) => {\n const count = episodesByType.get(ep.type) || 0;\n episodesByType.set(ep.type, count + 1);\n });\n\n const typeSummary = Array.from(episodesByType.entries())\n .map(([type, count]) => `${type}: ${count}`)\n .join(', ');\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Found ${episodes.length} total episodes on ${seriesUrl} (${typeSummary})`,\n );\n\n // Get download types from config\n const { downloadTypes } = config.check;\n\n // Filter for episodes matching download types and not yet downloaded\n const newEpisodes = episodes.filter((ep) => {\n const shouldDownload = downloadTypes.includes(ep.type as EpisodeType);\n // Get state path from config\n const statePath = config.stateFile;\n const notDownloaded = !this.stateManager.isDownloaded(statePath, seriesName, ep.number);\n return shouldDownload && notDownloaded;\n });\n\n // Log how many episodes will be downloaded\n if (episodes.length !== newEpisodes.length) {\n const skippedCount = episodes.length - newEpisodes.length;\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Filtering to ${downloadTypes.join(' or ')}: ${newEpisodes.length} episodes to download, ${skippedCount} skipped`,\n );\n }\n\n if (newEpisodes.length > 0) {\n return {\n hasNewEpisodes: true,\n episodes: newEpisodes,\n };\n }\n\n // No new episodes\n return {\n hasNewEpisodes: false,\n episodes: [],\n shouldRequeue: true,\n } as { hasNewEpisodes: false; episodes: Episode[]; requeueDelay?: number };\n }\n\n /**\n * Calculate exponential backoff with jitter\n *\n * @param retryCount - Current retry count\n * @param initialTimeout - Initial timeout in ms\n * @param backoffMultiplier - Multiplier for exponential backoff\n * @param jitterPercentage - Percentage of jitter (0-100)\n * @returns Delay in milliseconds\n */\n private calculateBackoff(\n retryCount: number,\n initialTimeout: number,\n backoffMultiplier: number,\n jitterPercentage: number,\n ): number {\n // Calculate base delay with exponential backoff\n const baseDelay = initialTimeout * backoffMultiplier ** retryCount;\n\n // Calculate jitter amount\n const jitterAmount = (baseDelay * jitterPercentage) / 100;\n\n // Generate random jitter within ±jitterAmount\n const jitter = (Math.random() * 2 - 1) * jitterAmount;\n\n // Calculate final delay (ensure non-negative)\n const finalDelay = Math.max(0, baseDelay + jitter);\n\n return Math.floor(finalDelay);\n }\n}\n","/**\n * TypedQueue - Passive queue for storing tasks of a single type\n *\n * A queue that manages tasks of a single type but does NOT execute them.\n * This is a passive data store that:\n * - Stores tasks in FIFO order\n * - Tracks cooldown time (next available timestamp)\n * - Tracks if a task of this type is currently executing\n * - Does NOT auto-start or have a processor function\n *\n * Key differences from AsyncQueue:\n * - No auto-start when items added\n * - No processor function (passive data store)\n * - Tracks cooldown from completion time\n * - No internal timing/sleep calls\n */\n\nexport type TaskItem<TaskType> = {\n data: TaskType;\n addedAt: Date;\n};\n\n/**\n * TypedQueue for a single task type\n */\nexport class TypedQueue<TaskType> {\n // State\n private queue: TaskItem<TaskType>[] = [];\n private isExecuting: boolean = false;\n private nextAvailableAt: Date = new Date(0); // Past = available\n private cooldownMs: number;\n\n /**\n * Create a new TypedQueue\n *\n * @param cooldownMs - Cooldown in milliseconds between task completions\n */\n constructor(cooldownMs: number = 0) {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Add a task to the queue\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n add(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.push({ data: task, addedAt });\n }\n\n /**\n * Add a task to the front of the queue (priority)\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addFirst(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.unshift({ data: task, addedAt });\n }\n\n /**\n * Get the next task from the queue\n *\n * @returns Next task or null if queue is empty\n */\n getNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n\n const item = this.queue.shift();\n return item?.data ?? null;\n }\n\n /**\n * Peek at the next task without removing it\n *\n * @returns Next task or null if queue is empty\n */\n peekNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n return this.queue[0]?.data ?? null;\n }\n\n /**\n * Check if a task can start at the given time\n *\n * @param now - Current time\n * @returns Whether task can start\n */\n canStart(now: Date): boolean {\n if (this.isExecuting) {\n return false;\n }\n\n if (now < this.nextAvailableAt) {\n return false;\n }\n\n // Check if head task is ready (respect delay)\n const head = this.queue[0];\n if (head && now < head.addedAt) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Mark a task as started\n */\n markStarted(): void {\n this.isExecuting = true;\n }\n\n /**\n * Mark a task as completed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markCompleted(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Mark a task as failed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markFailed(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Check if queue has tasks\n *\n * @returns Whether queue has tasks\n */\n hasTasks(): boolean {\n return this.queue.length > 0;\n }\n\n /**\n * Get the next time this queue can start a task\n *\n * @returns Next available time\n */\n getNextAvailableTime(): Date {\n // Start with cooldown time\n let time = this.nextAvailableAt;\n\n // Check head task delay\n const head = this.queue[0];\n if (head && head.addedAt > time) {\n time = head.addedAt;\n }\n\n return time;\n }\n\n /**\n * Get queue length\n *\n * @returns Number of tasks in queue\n */\n getQueueLength(): number {\n return this.queue.length;\n }\n\n /**\n * Check if a task is currently executing\n *\n * @returns Whether a task is executing\n */\n getIsExecuting(): boolean {\n return this.isExecuting;\n }\n\n /**\n * Get cooldown duration\n *\n * @returns Cooldown in milliseconds\n */\n getCooldownMs(): number {\n return this.cooldownMs;\n }\n\n /**\n * Set cooldown duration\n *\n * @param cooldownMs - New cooldown in milliseconds\n */\n setCooldownMs(cooldownMs: number): void {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Clear all tasks from the queue\n */\n clear(): void {\n this.queue = [];\n }\n\n /**\n * Get status information\n *\n * @returns Status object\n */\n getStatus(): {\n queueLength: number;\n isExecuting: boolean;\n nextAvailableAt: Date;\n cooldownMs: number;\n canStartNow: boolean;\n } {\n const now = new Date();\n return {\n queueLength: this.queue.length,\n isExecuting: this.isExecuting,\n nextAvailableAt: this.nextAvailableAt,\n cooldownMs: this.cooldownMs,\n canStartNow: this.canStart(now),\n };\n }\n}\n","/**\n * UniversalScheduler - Central scheduler for all typed queues\n *\n * Coordinates all typed queues with a single executor:\n * - Only one task executing globally\n * - Single active timer (cleared on scheduling attempt)\n * - Fair round-robin queue selection\n * - Event-driven (triggers on task add, completion, timer)\n *\n * Key features:\n * - Centralized scheduling logic\n * - Proper cooldowns (end-to-start timing)\n * - Reusable for any task type\n * - Timer-based instead of polling\n */\n\nimport { TypedQueue } from './typed-queue.js';\n\n/**\n * Executor callback function type\n */\nexport type ExecutorCallback<TaskType> = (task: TaskType, queueName: string) => Promise<void>;\n\n/**\n * Universal scheduler for coordinating all typed queues\n */\nexport class UniversalScheduler<TaskType> {\n // State\n private queues: Map<string, TypedQueue<TaskType>> = new Map();\n private queueCooldowns: Map<string, number> = new Map(); // Store default cooldown per queue\n private executorBusy: boolean = false;\n private timerId: ReturnType<typeof setTimeout> | null = null;\n private roundRobinIndex: number = 0;\n private stopped: boolean = false;\n\n // Callback\n private executor: ExecutorCallback<TaskType>;\n private onWait?: (queueName: string, waitMs: number, nextTime: Date) => void;\n\n /**\n * Create a new UniversalScheduler\n *\n * @param executor - Function to execute a task\n */\n constructor(executor: ExecutorCallback<TaskType>) {\n this.executor = executor;\n }\n\n /**\n * Set callback for when the scheduler is waiting\n *\n * @param callback - Callback function\n */\n setOnWait(callback: (queueName: string, waitMs: number, nextTime: Date) => void): void {\n this.onWait = callback;\n }\n\n /**\n * Register a new queue type\n *\n * @param typeName - Unique name for this queue type\n * @param cooldownMs - Default cooldown in milliseconds\n */\n registerQueue(typeName: string, cooldownMs: number): void {\n if (this.queues.has(typeName)) {\n throw new Error(`Queue ${typeName} is already registered`);\n }\n\n const queue = new TypedQueue<TaskType>(cooldownMs);\n this.queues.set(typeName, queue);\n this.queueCooldowns.set(typeName, cooldownMs);\n }\n\n /**\n * Check if a queue is registered\n *\n * @param typeName - Queue type name\n * @returns Whether queue is registered\n */\n hasQueue(typeName: string): boolean {\n return this.queues.has(typeName);\n }\n\n /**\n * Unregister a queue type\n *\n * @param typeName - Queue type name to unregister\n */\n unregisterQueue(typeName: string): void {\n this.queues.delete(typeName);\n this.queueCooldowns.delete(typeName);\n }\n\n /**\n * Add a task to a specific queue\n *\n * Triggers scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.add(task, delay);\n\n // Trigger scheduling attempt (might be executable immediately)\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Add a priority task to the front of a specific queue\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds\n */\n addPriorityTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.addFirst(task, delay);\n\n // Trigger scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as complete\n *\n * Called by executor when task completes successfully.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskComplete(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markCompleted(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as failed\n *\n * Called by executor when task fails.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskFailed(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markFailed(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Schedule the next task\n *\n * Attempts to schedule immediately if possible,\n * otherwise sets a timer for the earliest available time.\n */\n scheduleNext(): void {\n if (this.stopped) {\n return;\n }\n\n // Clear any existing timer\n this.clearTimer();\n\n // Try to schedule immediately\n const scheduled = this.trySchedule();\n\n if (scheduled) {\n // Task scheduled and executor is busy.\n // No need to set timer, completion will trigger next schedule.\n return;\n }\n\n // If executor is busy but nothing new was scheduled (because it was already busy),\n // we also don't need a timer.\n if (this.executorBusy) {\n return;\n }\n\n // No task running and none could be scheduled.\n // Check if we should set a timer for the next available time\n const next = this.getEarliestAvailableTime();\n if (next) {\n const now = Date.now();\n const waitMs = Math.max(0, next.time.getTime() - now);\n this.scheduleTimer(waitMs, next.queueName, next.time);\n }\n }\n\n /**\n * Try to schedule a task now\n *\n * @returns Whether a task was scheduled\n */\n private trySchedule(): boolean {\n // Can't schedule if executor is busy\n if (this.executorBusy) {\n return false;\n }\n\n const now = new Date();\n\n // Collect queue names for round-robin\n const queueNames = Array.from(this.queues.keys());\n if (queueNames.length === 0) {\n return false;\n }\n\n // Try each queue in round-robin order\n for (let i = 0; i < queueNames.length; i++) {\n const index = (this.roundRobinIndex + i) % queueNames.length;\n const queueName = queueNames[index];\n if (!queueName) continue;\n\n const queue = this.queues.get(queueName);\n if (!queue) continue;\n\n // Check if queue has tasks and can start\n if (queue.hasTasks() && queue.canStart(now)) {\n // Get next task\n const task = queue.getNext();\n if (task) {\n // Mark as started\n queue.markStarted();\n this.executorBusy = true;\n this.roundRobinIndex = (index + 1) % queueNames.length;\n\n // Execute task (fire and forget - executor will call back)\n this.executeTask(queueName, task).catch((error) => {\n // Execution failed - mark as failed and continue\n console.error(`[UniversalScheduler] Task execution failed: ${error}`);\n this.markTaskFailed(queueName);\n });\n\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Execute a task\n *\n * @param queueName - Queue name\n * @param task - Task to execute\n */\n private async executeTask(queueName: string, task: TaskType): Promise<void> {\n await this.executor(task, queueName);\n }\n\n /**\n * Schedule a timer for the next attempt\n *\n * @param waitMs - Milliseconds to wait\n * @param queueName - Name of the queue we are waiting for\n * @param nextTime - Time when the task will be ready\n */\n private scheduleTimer(waitMs: number, queueName: string, nextTime: Date): void {\n this.clearTimer();\n\n // Notify waiting state if callback defined and wait is significant (>1s)\n if (this.onWait && waitMs > 1000) {\n this.onWait(queueName, waitMs, nextTime);\n }\n\n this.timerId = setTimeout(() => {\n this.timerId = null;\n this.scheduleNext();\n }, waitMs);\n }\n\n /**\n * Clear the active timer\n */\n private clearTimer(): void {\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n }\n\n /**\n * Get the earliest available time across all queues\n *\n * @returns Earliest available time and queue name, or null if no queues with tasks\n */\n private getEarliestAvailableTime(): { time: Date; queueName: string } | null {\n let result: { time: Date; queueName: string } | null = null;\n\n for (const [name, queue] of this.queues.entries()) {\n // Only consider queues that have tasks\n if (!queue.hasTasks()) {\n continue;\n }\n\n const nextTime = queue.getNextAvailableTime();\n if (result === null || nextTime < result.time) {\n result = { time: nextTime, queueName: name };\n }\n }\n\n return result;\n }\n\n /**\n * Stop the scheduler\n *\n * Clears timers and prevents further scheduling.\n */\n stop(): void {\n this.stopped = true;\n this.clearTimer();\n }\n\n /**\n * Resume the scheduler\n */\n resume(): void {\n this.stopped = false;\n this.scheduleNext();\n }\n\n /**\n * Get statistics for all queues\n *\n * @returns Map of queue name to status\n */\n getStats(): Map<string, { queueLength: number; isExecuting: boolean; nextAvailableAt: Date }> {\n const stats = new Map();\n\n for (const [name, queue] of this.queues.entries()) {\n const status = queue.getStatus();\n stats.set(name, {\n queueLength: status.queueLength,\n isExecuting: status.isExecuting,\n nextAvailableAt: status.nextAvailableAt,\n });\n }\n\n return stats;\n }\n\n /**\n * Check if executor is busy\n *\n * @returns Whether executor is busy\n */\n isExecutorBusy(): boolean {\n return this.executorBusy;\n }\n\n /**\n * Check if there are any pending tasks\n *\n * @returns Whether there are pending tasks\n */\n hasPendingTasks(): boolean {\n for (const queue of this.queues.values()) {\n if (queue.hasTasks()) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get total pending tasks across all queues\n *\n * @returns Total pending task count\n */\n getTotalPendingTasks(): number {\n let total = 0;\n for (const queue of this.queues.values()) {\n total += queue.getQueueLength();\n }\n return total;\n }\n}\n","import parser from 'cron-parser';\n\n/**\n * Parse time string in HH:MM format\n *\n * @param timeStr - Time string in HH:MM format (e.g., \"20:00\")\n * @returns Date object with today's date and the specified time\n */\nexport function parseTime(timeStr: string): Date {\n const match = timeStr.match(/^(\\d{1,2}):(\\d{2})$/);\n if (!match) {\n throw new Error(`Invalid time format: \"${timeStr}\". Expected HH:MM`);\n }\n\n const [, hoursStr, minutesStr] = match;\n const hours = parseInt(hoursStr || '0', 10);\n const minutes = parseInt(minutesStr || '0', 10);\n\n if (hours < 0 || hours > 23) {\n throw new Error(`Invalid hours: ${hours}. Must be between 0 and 23`);\n }\n\n if (minutes < 0 || minutes > 59) {\n throw new Error(`Invalid minutes: ${minutes}. Must be between 0 and 59`);\n }\n\n const date = new Date();\n date.setHours(hours, minutes, 0, 0);\n return date;\n}\n\n/**\n * Get milliseconds until the next occurrence of a time\n *\n * @param timeStr - Time string in HH:MM format\n * @returns Milliseconds until the next occurrence\n */\nexport function getMsUntilTime(timeStr: string): number {\n const targetTime = parseTime(timeStr);\n const now = new Date();\n\n const targetDate = new Date(now);\n targetDate.setHours(targetTime.getHours(), targetTime.getMinutes(), 0, 0);\n\n // If the time has already passed today, schedule for tomorrow\n if (targetDate <= now) {\n targetDate.setDate(targetDate.getDate() + 1);\n }\n\n return targetDate.getTime() - now.getTime();\n}\n\n/**\n * Get milliseconds until the next occurrence of a cron expression\n *\n * @param cronExpression - Cron expression\n * @returns Milliseconds until the next occurrence\n */\nexport function getMsUntilCron(cronExpression: string): number {\n try {\n const interval = parser.parseExpression(cronExpression);\n const nextDate = interval.next().toDate();\n const now = new Date();\n return nextDate.getTime() - now.getTime();\n } catch (err) {\n throw new Error(\n `Invalid cron expression: \"${cronExpression}\". Error: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Format duration in human-readable format\n *\n * @param ms - Duration in milliseconds\n * @returns Formatted duration string\n */\nexport function formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d ${hours % 24}h`;\n }\n if (hours > 0) {\n return `${hours}h ${minutes % 60}m`;\n }\n if (minutes > 0) {\n return `${minutes}m ${seconds % 60}s`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Sleep for specified milliseconds\n *\n * @param ms - Milliseconds to sleep\n * @returns Promise that resolves after the sleep\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Scheduler - Queue-based architecture for managing series checks\n *\n * Features:\n * - Per-domain sequential processing (concatMap semantics)\n * - Domain-based parallelism\n * - Retry with exponential backoff\n * - \"No episodes\" requeue with interval\n * - Graceful shutdown\n */\n\nimport { AppContext } from '../app-context.js';\nimport type { SeriesConfig } from '../config/config-schema.js';\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { SchedulerError } from '../errors/custom-errors.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport { QueueManager } from '../queue/queue-manager.js';\nimport type { SchedulerOptions } from '../types/config.types.js';\nimport { getMsUntilCron, getMsUntilTime, sleep } from '../utils/time-utils.js';\n\n/**\n * Time provider type for dependency injection\n */\nexport type TimeProvider = {\n getMsUntilTime: typeof getMsUntilTime;\n getMsUntilCron: typeof getMsUntilCron;\n sleep: typeof sleep;\n};\n\n/**\n * QueueManager factory type for dependency injection\n */\nexport type QueueManagerFactory = (downloadManager: DownloadManager) => QueueManager;\n\n/**\n * Scheduler for managing periodic checks with queue-based architecture\n */\nexport class Scheduler {\n private configs: SeriesConfig[];\n private downloadManager: DownloadManager;\n private options: SchedulerOptions;\n private queueManager: QueueManager;\n private running: boolean = false;\n private stopped: boolean = true;\n private timeProvider: TimeProvider;\n private scheduleTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n configs: SeriesConfig[],\n downloadManager: DownloadManager,\n options: SchedulerOptions = { mode: 'scheduled' },\n timeProvider?: TimeProvider,\n queueManagerFactory?: QueueManagerFactory,\n ) {\n this.configs = configs;\n this.downloadManager = downloadManager;\n this.options = options;\n this.timeProvider = timeProvider || { getMsUntilTime, getMsUntilCron, sleep };\n\n // Create queue manager\n const createQueueManager = queueManagerFactory || ((dm) => new QueueManager(dm));\n\n this.queueManager = createQueueManager(this.downloadManager);\n }\n\n /**\n * Start the scheduler\n */\n async start(): Promise<void> {\n if (this.running) {\n throw new SchedulerError('Scheduler is already running');\n }\n\n this.running = true;\n this.stopped = false;\n\n // Start queue manager\n this.queueManager.start();\n\n const notifier = AppContext.getNotifier();\n\n if (this.options.mode === 'once') {\n notifier.notify(NotificationLevel.INFO, 'Single-run mode: checking all series once');\n await this.runOnce();\n this.running = false;\n } else {\n notifier.notify(NotificationLevel.INFO, 'Scheduler started (queue-based architecture)');\n this.scheduleNextBatch();\n\n // Keep promise pending forever for scheduled mode to prevent process exit\n // In a real app, this is handled by the event loop being active (timers/intervals)\n // but runApp awaits start(), so we return a promise that only resolves on stop()\n return new Promise<void>((resolve) => {\n const checkStop = setInterval(() => {\n if (!this.running) {\n clearInterval(checkStop);\n resolve();\n }\n }, 100);\n });\n }\n }\n\n private scheduleNextBatch(): void {\n if (this.stopped) return;\n\n const notifier = AppContext.getNotifier();\n const groupedConfigs = this.groupConfigsBySchedule();\n let nextScheduleKey: string | null = null;\n let minMsUntil = Number.MAX_SAFE_INTEGER;\n\n for (const scheduleKey of groupedConfigs.keys()) {\n let msUntil: number;\n\n try {\n // Determine if it's HH:MM or cron\n if (/^\\d{1,2}:\\d{2}$/.test(scheduleKey)) {\n msUntil = this.timeProvider.getMsUntilTime(scheduleKey);\n } else {\n // Assume cron\n msUntil = this.timeProvider.getMsUntilCron(scheduleKey);\n }\n\n if (msUntil < minMsUntil) {\n minMsUntil = msUntil;\n nextScheduleKey = scheduleKey;\n }\n } catch (error) {\n notifier.notify(\n NotificationLevel.ERROR,\n `Error calculating next run time for schedule \"${scheduleKey}\": ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n if (!nextScheduleKey) {\n notifier.notify(NotificationLevel.WARNING, 'No scheduled configs found.');\n return;\n }\n\n const configs = groupedConfigs.get(nextScheduleKey);\n if (!configs) return;\n\n if (minMsUntil > 0) {\n this.options.onIdle?.();\n notifier.notify(\n NotificationLevel.INFO,\n `Waiting ${Math.floor(minMsUntil / 1000 / 60)} minutes until next run (${nextScheduleKey})...`,\n );\n }\n\n // Schedule next run\n this.scheduleTimer = setTimeout(async () => {\n if (this.stopped) return;\n await this.runConfigs(configs);\n await this.waitForQueueDrain();\n this.scheduleNextBatch();\n }, minMsUntil);\n }\n\n /**\n * Wait for all queues to drain\n */\n private async waitForQueueDrain(): Promise<void> {\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n }\n\n /**\n * Stop the scheduler\n */\n async stop(): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Stopping scheduler...');\n\n this.stopped = true;\n if (this.scheduleTimer) {\n clearTimeout(this.scheduleTimer);\n this.scheduleTimer = null;\n }\n\n // Stop queue manager (drains all queues)\n await this.queueManager.stop();\n\n this.running = false;\n\n notifier.notify(NotificationLevel.INFO, 'Scheduler stopped');\n }\n\n /**\n * Reload configuration\n */\n async reload(configs: SeriesConfig[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Reloading configuration...');\n\n // Update internal state\n this.configs = configs;\n\n // Update queue manager config (reloads from AppContext)\n this.queueManager.updateConfig();\n\n // If running in scheduled mode, restart the schedule\n if (this.running && this.options.mode === 'scheduled') {\n if (this.scheduleTimer) {\n clearTimeout(this.scheduleTimer);\n this.scheduleTimer = null;\n }\n this.scheduleNextBatch();\n }\n\n notifier.notify(NotificationLevel.SUCCESS, 'Configuration reloaded');\n }\n\n /**\n * Update the download manager instance\n * Used during config reload when download settings change\n */\n updateDownloadManager(downloadManager: DownloadManager): void {\n this.downloadManager = downloadManager;\n // QueueManager gets DownloadManager through AppContext, no need to update\n }\n\n /**\n * Trigger immediate checks for all series\n */\n async triggerAllChecks(): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Triggering immediate checks for all series...');\n for (const config of this.configs) {\n this.queueManager.addSeriesCheck(config.url);\n }\n }\n\n /**\n * Group configs by schedule (startTime or cron)\n */\n private groupConfigsBySchedule(): Map<string, SeriesConfig[]> {\n const notifier = AppContext.getNotifier();\n const grouped = new Map<string, SeriesConfig[]>();\n\n for (const config of this.configs) {\n const scheduleKey = config.cron || config.startTime;\n if (!scheduleKey) {\n notifier.notify(\n NotificationLevel.WARNING,\n `Series \"${config.name}\" has no startTime or cron configured. Skipping.`,\n );\n continue;\n }\n\n const existing = grouped.get(scheduleKey) || [];\n existing.push(config);\n grouped.set(scheduleKey, existing);\n }\n\n return grouped;\n }\n\n /**\n * Add all configs to queue manager\n */\n private async runConfigs(configs: SeriesConfig[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n // Add all series to the queue manager\n for (const config of configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config.url);\n }\n\n // Log queue stats\n const stats = this.queueManager.getQueueStats();\n notifier.notify(\n NotificationLevel.INFO,\n `Added ${configs.length} series to check queues. Queue stats: ${JSON.stringify(stats)}`,\n );\n }\n\n /**\n * Run all configs in single-run mode\n */\n private async runOnce(): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n for (const config of this.configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config.url);\n }\n\n // Wait for all queues to drain\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n\n notifier.notify(NotificationLevel.SUCCESS, 'Single-run complete');\n }\n\n /**\n * Check if scheduler is running\n */\n isRunning(): boolean {\n return this.running && !this.stopped;\n }\n\n /**\n * Get queue manager (for testing/debugging)\n */\n getQueueManager(): QueueManager {\n return this.queueManager;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { CookieError } from '../errors/custom-errors';\n\n/**\n * Get browser cookie database path\n */\nfunction getBrowserPath(browser: string): string {\n const home = homedir();\n const platform = process.platform;\n\n const paths: Record<string, Record<string, string>> = {\n darwin: {\n chrome: join(home, 'Library/Application Support/Google/Chrome/Default/Cookies'),\n chromium: join(home, 'Library/Application Support/Chromium/Default/Cookies'),\n edge: join(home, 'Library/Application Support/Microsoft Edge/Default/Cookies'),\n firefox: join(home, 'Library/Application Support/Firefox/Profiles'),\n safari: join(home, 'Library/Cookies/Cookies.binarycookies'),\n },\n linux: {\n chrome: join(home, '.config/google-chrome/Default/Cookies'),\n chromium: join(home, '.config/chromium/Default/Cookies'),\n edge: join(home, '.config/microsoft-edge/Default/Cookies'),\n firefox: join(home, '.mozilla/firefox'),\n safari: '', // Safari not on Linux\n },\n win32: {\n chrome: join(home, 'AppData/Local/Google/Chrome/User Data/Default/Cookies'),\n chromium: join(home, 'AppData/Local/Chromium/User Data/Default/Cookies'),\n edge: join(home, 'AppData/Local/Microsoft/Edge/User Data/Default/Cookies'),\n firefox: join(home, 'AppData/Roaming/Mozilla/Firefox/Profiles'),\n safari: '', // Safari not on Windows\n },\n };\n\n const browserPaths = paths[platform];\n if (!browserPaths) {\n throw new CookieError(`Unsupported platform: ${platform}`);\n }\n\n const path = browserPaths[browser];\n if (!path) {\n throw new CookieError(`Browser \"${browser}\" not supported on ${platform}`);\n }\n\n return path;\n}\n\n/**\n * Extract cookies from browser for a specific domain\n * This is a simplified version - in production, you'd use a proper SQLite parser\n * or a library like `tough-cookie-file-store`\n *\n * @param domain - Domain to extract cookies for (e.g., \"wetv.vip\")\n * @param browser - Browser to extract from\n * @returns Cookie string in Netscape format\n */\nexport async function extractCookies(domain: string, browser: string = 'chrome'): Promise<string> {\n const cookiePath = getBrowserPath(browser);\n\n if (!existsSync(cookiePath)) {\n throw new CookieError(\n `Cookie database not found at \"${cookiePath}\". ` +\n `Make sure ${browser} is installed and you've logged in to the site.`,\n );\n }\n\n // For now, we'll use a simpler approach: tell the user to export cookies manually\n // In production, you'd use a proper SQLite parser here\n throw new CookieError(\n `Automatic cookie extraction is not yet implemented for ${browser}. ` +\n `Please export cookies manually:\\n` +\n `1. Install a browser extension like \"Get cookies.txt LOCALLY\"\\n` +\n `2. Go to ${domain} and log in\\n` +\n `3. Export cookies to a file\\n` +\n `4. Set 'cookieFile' in config.yaml to the exported file path`,\n );\n}\n\n/**\n * Read cookies from a Netscape-format cookie file\n *\n * @param cookieFile - Path to cookie file\n * @returns Cookie string for HTTP requests\n */\nexport async function readCookieFile(cookieFile: string): Promise<string> {\n if (!existsSync(cookieFile)) {\n throw new CookieError(`Cookie file not found: \"${cookieFile}\"`);\n }\n\n const content = await readFile(cookieFile, 'utf-8');\n\n // Parse Netscape cookie format and convert to Cookie header format\n const lines = content.split('\\n');\n const cookies: string[] = [];\n\n for (const line of lines) {\n // Skip comments and empty lines\n const trimmedLine = line.trim();\n if (trimmedLine.startsWith('#') || !trimmedLine) continue;\n\n const fields = line.split('\\t');\n if (fields.length >= 7) {\n const name = fields[5];\n const value = fields[6];\n\n if (name && value) {\n const cleanValue = value.trim();\n if (cleanValue) {\n cookies.push(`${name}=${cleanValue}`);\n }\n }\n }\n }\n\n return cookies.join('; ');\n}\n"],"mappings":"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","names":["fileURLToPath","run","readline","boolean","command","flag","option","string","existsSync","readFileSync","writeFile","isAbsolute","join","createEmptyState","StateManager","_StateManager","notifier","statePath","seriesName","episodeNumber","episodes","paddedNumber","error","state","episodeStr","fn","currentLock","lockPromise","fullPath","existsSync","createEmptyState","fileContent","readFileSync","sortedSeries","key","content","writeFile","isAbsolute","join","message","errorMessage","fullMessage","AppContext","_AppContext","configRegistry","notifier","stateManager","StateManager","existsSync","readFile","join","yaml","WetvloError","message","ConfigError","HandlerError","WetvloError","message","url","DownloadError","NotificationError","CookieError","SchedulerError","resolveEnv","value","_match","varName","envValue","resolveEnvRecursive","obj","item","result","key","z","EpisodeTypeSchema","CheckSettingsSchema","DownloadSettingsSchema","TelegramConfigSchema","BrowserSchema","CommonSettingsSchema","GlobalConfigSchema","DomainConfigSchema","SeriesConfigSchema","ConfigSchema","validateConfig","rawConfig","DEFAULT_CONFIG_PATH","loadConfig","configPath","absolutePath","join","existsSync","ConfigError","content","readFile","rawConfig","error","validateConfig","resolveEnvRecursive","deepMerge","target","source","result","key","sourceValue","targetValue","isObject","item","extractDomain","url","defaults","getDefaults","ConfigRegistry","root","defaults","getDefaults","globalMerged","deepMerge","dc","domainMerged","sc","hostname","extractDomain","globalConfig","seriesMerged","key","config","url","level","domain","resolved","domains","check","download","fs","fsPromises","basename","join","resolve","sanitizeFilename","name","execa","colors","Logger","config","level","date","month","day","hour","min","sec","message","timestamp","emoji","text","color","levels","logger","getVideoDuration","filePath","stdout","execa","duration","error","logger","extractDownloadOptions","resolvedConfig","fsPromises","join","execa","BaseDownloader","YtDlpDownloader","BaseDownloader","_url","episode","dir","filenameWithoutExt","options","outputTemplate","join","args","filename","allFiles","outputBuffer","subprocess","execa","line","text","destMatch","subMatch","mergeMatch","progressMatch","percentage","totalSize","speed","eta","error","errorMsg","fullLog","DownloaderRegistry","YtDlpDownloader","downloader","url","downloaderRegistry","escapeRegExp","string","DownloadManager","AppContext","seriesUrl","episode","notifier","resolved","statePath","seriesName","downloadOptions","extractDownloadOptions","downloader","downloaderRegistry","paddedNumber","filenameWithoutExt","sanitizeFilename","targetDir","result","progress","message","fileSize","fullPath","resolve","duration","getVideoDuration","file","absFile","fileName","basename","newPath","join","e","error","DownloadError","files","dir","absDir","pattern","cleanedCount","filePath","filename","bytes","units","size","unit","YtDlpDownloader","Registry","handler","url","domain","extractDomain","handlerDomain","HandlerError","handlerRegistry","cheerio","BaseHandler","url","domain","extractDomain","cookies","headers","response","HandlerError","error","html","text","chineseMatch","epMatch","episodeMatch","numberMatch","element","$","$el","className","IQiyiHandler","BaseHandler","url","cookies","html","nextDataEpisodes","episodes","match","dataStr","pageData","albumInfo","videoList","albumId","title","video","vid","episode","isTrailer","payStatus","episodeNumber","episodeUrl","type","error","a","b","$","selectors","selector","links","_","element","$el","href","text","ep","$parent","MGTVHandler","BaseHandler","url","cookies","videoId","episodes","page","totalPages","apiUrl","response","data","item","episodeNumber","episodeUrl","match","uniqueEpisodes","episode","a","b","WeTVHandler","BaseHandler","url","cookies","html","nextDataEpisodes","episodes","match","dataStr","pageData","coverInfo","videoList","cid","title","coverId","video","vid","episode","isTrailer","episodeNumber","encodedTitle","episodeUrl","type","error","a","b","$","episodeLinks","_","el","element","labels","payStatus","defaultPayStatus","key","label","labelText","status","$el","href","ariaLabel","ep","$li","badge","badgeText","liText","ConsoleNotifier","level","message","logger","TelegramNotifier","config","level","message","emoji","MAX_LENGTH","safeMessage","escapedMessage","formattedMessage","response","errorText","NotificationError","error","text","_message","createHash","TypedQueue","cooldownMs","task","delay","addedAt","now","head","time","UniversalScheduler","executor","callback","typeName","cooldownMs","queue","TypedQueue","task","delay","actualCooldown","next","now","waitMs","queueNames","i","index","queueName","error","nextTime","result","name","stats","status","total","QueueManager","downloadManager","schedulerFactory","AppContext","createScheduler","executor","UniversalScheduler","task","queueName","waitMs","notifier","seconds","parts","type","domain","seriesUrl","registry","extractDomain","seriesName","item","episodes","resolvedConfig","downloadDelay","i","episode","delayMs","stats","checkQueues","downloadQueues","queueStats","testUrl","hash","createHash","checkInterval","handler","handlerRegistry","attemptNumber","retryCount","checksCount","result","intervalMs","requeueDelay","requeuedItem","error","errorMessage","maxRetries","initialTimeout","backoffMultiplier","jitterPercentage","retryDelay","config","episodesByType","ep","count","typeSummary","downloadTypes","newEpisodes","shouldDownload","statePath","notDownloaded","skippedCount","baseDelay","jitterAmount","jitter","finalDelay","parser","parseTime","timeStr","match","hoursStr","minutesStr","hours","minutes","date","getMsUntilTime","targetTime","now","targetDate","getMsUntilCron","cronExpression","nextDate","err","sleep","ms","resolve","Scheduler","configs","downloadManager","options","timeProvider","queueManagerFactory","getMsUntilTime","getMsUntilCron","sleep","createQueueManager","dm","QueueManager","SchedulerError","notifier","AppContext","resolve","checkStop","groupedConfigs","nextScheduleKey","minMsUntil","scheduleKey","msUntil","error","config","grouped","existing","stats","existsSync","readFile","homedir","join","readCookieFile","cookieFile","existsSync","CookieError","lines","readFile","cookies","line","trimmedLine","fields","name","value","cleanValue","defaultDependencies","loadConfig","DownloadManager","readCookieFile","c","dm","opt","Scheduler","handleShutdown","scheduler","logger","error","runApp","configPath","mode","deps","config","configRegistry","ConfigRegistry","_globalConfig","createNotifier","registry","notifiers","ConsoleNotifier","cfg","TelegramNotifier","level","message","n","notifier","stateManager","StateManager","AppContext","handlerRegistry","WeTVHandler","IQiyiHandler","MGTVHandler","downloadManager","onIdle","_str","key","name","newConfig","newConfigRegistry","newGlobalConfig","newNotifier","cli","command","option","string","flag","boolean","once","ConfigError","main","args","run","cli","isMainModule","fileURLToPath"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/app.ts","../src/state/state-manager.ts","../src/types/state.types.ts","../src/app-context.ts","../src/config/config-loader.ts","../src/errors/custom-errors.ts","../src/utils/env-resolver.ts","../src/config/config-schema.ts","../src/utils/deep-merge.ts","../src/utils/url-utils.ts","../src/config/config-defaults.ts","../src/config/config-registry.ts","../src/downloader/download-manager.ts","../src/utils/filename-sanitizer.ts","../src/utils/video-validator.ts","../src/utils/logger.ts","../src/downloader/download-options.ts","../src/downloader/base-downloader.ts","../src/downloader/lib/ytdlp-wrapper.ts","../src/downloader/impl/yt-dlp-downloader.ts","../src/downloader/downloader-registry.ts","../src/handlers/handler-registry.ts","../src/handlers/base/base-handler.ts","../src/handlers/impl/iqiyi-handler.ts","../src/handlers/impl/mgtv-handler.ts","../src/handlers/impl/wetv-handler.ts","../src/handlers/impl/youku-handler.ts","../src/notifications/console-notifier.ts","../src/notifications/telegram-notifier.ts","../src/queue/queue-manager.ts","../src/queue/typed-queue.ts","../src/queue/universal-scheduler.ts","../src/utils/time-utils.ts","../src/scheduler/scheduler.ts","../src/utils/cookie-extractor.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { run } from 'cmd-ts';\nimport { cli } from './app.js';\n\n/**\n * Main entry point\n */\nexport async function main(args: string[] = process.argv.slice(2)): Promise<void> {\n await run(cli, args);\n}\n\n// Check if running directly in Node.js or Bun\nconst isMainModule = import.meta.main || (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url));\n\nif (isMainModule) {\n await main();\n}\n","import * as readline from 'node:readline';\nimport { boolean, command, flag, option, string } from 'cmd-ts';\nimport { AppContext } from './app-context.js';\nimport { loadConfig } from './config/config-loader.js';\nimport { ConfigRegistry } from './config/config-registry.js';\nimport type { SeriesConfig } from './config/config-schema.js';\nimport { DownloadManager } from './downloader/download-manager.js';\nimport { ConfigError } from './errors/custom-errors.js';\nimport { handlerRegistry } from './handlers/handler-registry.js';\nimport { IQiyiHandler } from './handlers/impl/iqiyi-handler.js';\nimport { MGTVHandler } from './handlers/impl/mgtv-handler.js';\nimport { WeTVHandler } from './handlers/impl/wetv-handler.js';\nimport { YoukuHandler } from './handlers/impl/youku-handler.js';\nimport { ConsoleNotifier } from './notifications/console-notifier.js';\nimport type { NotificationLevel, Notifier } from './notifications/notifier.js';\nimport { TelegramNotifier } from './notifications/telegram-notifier.js';\nimport { Scheduler } from './scheduler/scheduler.js';\nimport { StateManager } from './state/state-manager.js';\nimport type { SchedulerMode, SchedulerOptions } from './types/config.types.js';\nimport { readCookieFile } from './utils/cookie-extractor.js';\nimport { logger } from './utils/logger.js';\n\nexport type AppDependencies = {\n loadConfig: typeof loadConfig;\n checkYtDlpInstalled: () => Promise<boolean>;\n readCookieFile: typeof readCookieFile;\n createDownloadManager: () => DownloadManager;\n createScheduler: (configs: SeriesConfig[], downloadManager: DownloadManager, options?: SchedulerOptions) => Scheduler;\n};\n\nconst defaultDependencies: AppDependencies = {\n loadConfig,\n checkYtDlpInstalled: DownloadManager.checkYtDlpInstalled,\n readCookieFile,\n createDownloadManager: () => new DownloadManager(),\n createScheduler: (c, dm, opt) => new Scheduler(c, dm, opt),\n};\n\n/**\n * Handle graceful shutdown\n */\nexport async function handleShutdown(scheduler: Scheduler): Promise<void> {\n logger.info('Shutting down gracefully...');\n\n try {\n await scheduler.stop();\n logger.success('Shutdown complete');\n } catch (error) {\n logger.error(`Error during shutdown: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nexport async function runApp(\n configPath: string,\n mode: SchedulerMode,\n deps: AppDependencies = defaultDependencies,\n): Promise<void> {\n logger.info(`Mode: ${mode === 'once' ? 'Single-run (checks once, exits)' : 'Scheduled (waits for startTime)'}`);\n\n // Check if yt-dlp is installed\n logger.info('Checking yt-dlp installation...');\n const ytDlpInstalled = await deps.checkYtDlpInstalled();\n\n if (!ytDlpInstalled) {\n throw new Error(\n 'yt-dlp is not installed. Please install it first:\\n' +\n ' - macOS: brew install yt-dlp\\n' +\n ' - Linux: pip install yt-dlp\\n' +\n ' - Windows: winget install yt-dlp',\n );\n }\n\n // Load configuration\n logger.info(`Loading configuration from ${configPath}...`);\n const config = await deps.loadConfig(configPath);\n logger.success('Configuration loaded');\n\n // Create config registry\n const configRegistry = new ConfigRegistry(config);\n\n // Get global config (stored in let for config reload comparison)\n let _globalConfig = configRegistry.getConfig('global');\n\n /**\n * Create notifier instance from config\n * Extracted to factory function for reuse during config reload\n */\n const createNotifier = (registry: ConfigRegistry): Notifier => {\n const notifiers: Array<ConsoleNotifier | TelegramNotifier> = [new ConsoleNotifier()];\n const cfg = registry.getConfig('global');\n\n if (cfg.telegram) {\n try {\n notifiers.push(new TelegramNotifier(cfg.telegram));\n logger.info('Telegram notifications enabled for errors');\n } catch (error) {\n logger.warning(`Failed to set up Telegram: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n // Create composite notifier\n return {\n notify: async (level: NotificationLevel, message: string): Promise<void> => {\n await Promise.all(notifiers.map((n) => n.notify(level, message)));\n },\n progress: (message: string): void => {\n for (const n of notifiers) {\n n.progress(message);\n }\n },\n endProgress: (): void => {\n for (const n of notifiers) {\n n.endProgress();\n }\n },\n };\n };\n\n const notifier = createNotifier(configRegistry);\n\n // Create state manager\n const stateManager = new StateManager(notifier);\n\n // Initialize AppContext with all services\n AppContext.initialize(configRegistry, notifier, stateManager);\n logger.info('AppContext initialized');\n\n // Register handlers\n handlerRegistry.register(new WeTVHandler());\n handlerRegistry.register(new IQiyiHandler());\n handlerRegistry.register(new MGTVHandler());\n handlerRegistry.register(new YoukuHandler());\n logger.info(`Registered handlers: ${handlerRegistry.getDomains().join(', ')}`);\n\n // Create download manager\n const downloadManager = deps.createDownloadManager();\n\n // Setup interactive mode instructions\n let onIdle: (() => void) | undefined;\n if (mode === 'scheduled' && process.stdin.isTTY) {\n const printInstructions = () => {\n logger.info('Interactive mode enabled:');\n logger.info(' [r] Reload configuration');\n logger.info(' [c] Trigger immediate checks');\n logger.info(' [q] Quit');\n };\n\n onIdle = printInstructions;\n }\n\n // Create and start scheduler with queue-based architecture\n logger.info('Using queue-based scheduler');\n const scheduler = deps.createScheduler(config.series, downloadManager, { mode, onIdle });\n\n // Set up signal handlers for graceful shutdown\n process.on('SIGINT', async () => {\n await handleShutdown(scheduler);\n process.exit(0);\n });\n process.on('SIGTERM', async () => {\n await handleShutdown(scheduler);\n process.exit(0);\n });\n\n // Setup keyboard input listeners\n if (mode === 'scheduled' && process.stdin.isTTY) {\n readline.emitKeypressEvents(process.stdin);\n process.stdin.setRawMode(true);\n\n process.stdin.on('keypress', async (str, key) => {\n if (!key) return;\n\n const name = key.name || '';\n const char = str || '';\n\n // q, й or Ctrl+C to quit\n if (name === 'q' || name === 'й' || char === 'й' || (key.ctrl && name === 'c')) {\n await handleShutdown(scheduler);\n process.exit(0);\n }\n // r or к to reload config\n else if (name === 'r' || name === 'к' || char === 'к') {\n try {\n logger.info(`Reloading configuration from ${configPath}...`);\n const newConfig = await deps.loadConfig(configPath);\n const newConfigRegistry = new ConfigRegistry(newConfig);\n const newGlobalConfig = newConfigRegistry.getConfig('global');\n\n // Reload notifier (Telegram settings, etc.)\n const newNotifier = createNotifier(newConfigRegistry);\n AppContext.setNotifier(newNotifier);\n\n // Update global config reference\n _globalConfig = newGlobalConfig;\n\n // Reload config registry and scheduler\n AppContext.reloadConfig(newConfigRegistry);\n await scheduler.reload(newConfig.series);\n\n logger.success('Configuration reloaded successfully');\n } catch (error) {\n logger.error(`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n // c or с (Cyrillic) to trigger checks\n else if (name === 'c' || name === 'с' || char === 'с') {\n await scheduler.triggerAllChecks();\n }\n });\n }\n\n // Start the scheduler\n await scheduler.start();\n}\n\n// Define CLI using cmd-ts\nexport const cli = command({\n name: 'wetvlo',\n description: 'CLI Video Downloader for Chinese streaming sites',\n version: '0.0.1',\n args: {\n config: option({\n type: string,\n long: 'config',\n short: 'c',\n defaultValue: () => './config.yaml',\n description: 'Path to configuration file (default: ./config.yaml)',\n }),\n once: flag({\n type: boolean,\n long: 'once',\n short: 'o',\n description: 'Run in single-run mode (check once and exit)',\n }),\n },\n handler: async ({ config, once }: { config: string; once: boolean }) => {\n try {\n const mode: SchedulerMode = once ? 'once' : 'scheduled';\n await runApp(config, mode);\n } catch (error) {\n if (error instanceof ConfigError) {\n logger.error(`Configuration error: ${error.message}`);\n } else {\n logger.error(`Fatal error: ${error instanceof Error ? error.message : String(error)}`);\n }\n process.exit(1);\n }\n },\n});\n","import { existsSync, readFileSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\nimport type { Notifier } from '../notifications/notifier';\nimport { NotificationLevel } from '../notifications/notifier';\nimport type { State } from '../types/state.types';\nimport { createEmptyState } from '../types/state.types';\n\n/**\n * Episode number type (zero-padded string, e.g., \"01\", \"02\")\n */\nexport type EpisodeNumber = string;\n\n/**\n * State manager class for tracking downloaded episodes (v3.0.0 - file-based)\n *\n * This implementation does NOT keep state in memory. Every operation reads/writes\n * the file directly, with mutex protection to ensure atomicity.\n *\n * Usage:\n * const stateManager = AppContext.getStateManager();\n * const statePath = resolveStatePath(config);\n * stateManager.isDownloaded(statePath, seriesName, episodeNumber);\n * await stateManager.addDownloadedEpisode(statePath, seriesName, episodeNumber);\n */\nexport class StateManager {\n private static locks = new Map<string, Promise<void>>();\n private notifier?: Notifier;\n\n constructor(notifier?: Notifier) {\n this.notifier = notifier;\n }\n\n /**\n * Check if an episode has been downloaded\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @param episodeNumber - Episode number\n * @returns Whether the episode is downloaded\n */\n isDownloaded(statePath: string, seriesName: string, episodeNumber: number): boolean {\n try {\n const state = this.loadState(statePath);\n const episodes = state.series[seriesName];\n if (!episodes) return false;\n\n const paddedNumber = String(episodeNumber).padStart(2, '0');\n return episodes.includes(paddedNumber);\n } catch (error) {\n this.handleError(error, `Failed to check episode status for ${seriesName}`);\n return false;\n }\n }\n\n /**\n * Add a downloaded episode to state (atomic operation: read → modify → write)\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @param episodeNumber - Episode number\n */\n async addDownloadedEpisode(statePath: string, seriesName: string, episodeNumber: number): Promise<void> {\n return this.withLock(statePath, async () => {\n try {\n const state = this.loadState(statePath);\n\n if (!state.series[seriesName]) {\n state.series[seriesName] = [];\n }\n\n const episodeStr = String(episodeNumber).padStart(2, '0');\n if (!state.series[seriesName].includes(episodeStr)) {\n state.series[seriesName].push(episodeStr);\n state.series[seriesName].sort();\n }\n\n await this.saveState(statePath, state);\n } catch (error) {\n this.handleError(error, `Failed to add episode for ${seriesName}`);\n throw error;\n }\n });\n }\n\n /**\n * Get all episodes for a series\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param seriesName - Series name\n * @returns Array of episode numbers (as zero-padded strings)\n */\n getSeriesEpisodes(statePath: string, seriesName: string): EpisodeNumber[] {\n try {\n const state = this.loadState(statePath);\n return state.series[seriesName] ?? [];\n } catch (error) {\n this.handleError(error, `Failed to get episodes for ${seriesName}`);\n return [];\n }\n }\n\n /**\n * Execute a function with mutex lock for a specific state file\n *\n * @param statePath - Path to state file (used as lock key)\n * @param fn - Function to execute while holding the lock\n * @returns Result of the function\n */\n private async withLock<T>(statePath: string, fn: () => Promise<T>): Promise<T> {\n // Wait for previous operation to complete\n let currentLock = StateManager.locks.get(statePath);\n while (currentLock) {\n await currentLock;\n currentLock = StateManager.locks.get(statePath);\n }\n\n // Create a new lock\n const lockPromise = (async () => {\n try {\n return await fn();\n } finally {\n StateManager.locks.delete(statePath);\n }\n })();\n\n // @ts-expect-error - T extends void is guaranteed by usage\n StateManager.locks.set(statePath, lockPromise);\n return lockPromise;\n }\n\n /**\n * Load state from file\n *\n * @param statePath - Path to state file (relative or absolute)\n * @returns State object\n */\n private loadState(statePath: string): State {\n const fullPath = this.resolvePath(statePath);\n\n if (!existsSync(fullPath)) {\n return createEmptyState();\n }\n\n try {\n // Use synchronous read for isDownloaded (non-async method)\n const fileContent = readFileSync(fullPath, 'utf-8');\n return JSON.parse(fileContent) as State;\n } catch (error) {\n throw new Error(\n `Failed to load state from ${fullPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Save state to file\n *\n * @param statePath - Path to state file (relative or absolute)\n * @param state - State object to save\n */\n private async saveState(statePath: string, state: State): Promise<void> {\n const fullPath = this.resolvePath(statePath);\n\n try {\n // Sort series keys and episode numbers\n const sortedSeries: Record<string, string[]> = {};\n Object.keys(state.series)\n .sort()\n .forEach((key) => {\n const episodes = state.series[key];\n if (episodes) {\n sortedSeries[key] = [...episodes].sort();\n }\n });\n\n state.series = sortedSeries;\n\n const content = JSON.stringify(state, null, 2);\n await writeFile(fullPath, content, 'utf-8');\n } catch (error) {\n throw new Error(`Failed to save state to ${fullPath}: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n\n /**\n * Resolve state path to absolute path\n *\n * @param statePath - Path to state file (relative or absolute)\n * @returns Absolute path\n */\n private resolvePath(statePath: string): string {\n // If already absolute, return as-is\n if (isAbsolute(statePath)) {\n return statePath;\n }\n // Otherwise, resolve relative to current working directory\n return join(process.cwd(), statePath);\n }\n\n /**\n * Handle errors through notifier\n *\n * @param error - Error object\n * @param message - Error message prefix\n */\n private handleError(error: unknown, message: string): void {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const fullMessage = `${message}: ${errorMessage}`;\n\n if (this.notifier) {\n this.notifier.notify(NotificationLevel.ERROR, fullMessage);\n } else {\n console.error(fullMessage);\n }\n }\n}\n","/**\n * State file structure (v3.0.0)\n */\nexport type State = {\n /** State format version */\n version: string;\n /** Series keyed by Series Name, values are sorted lists of episode numbers (e.g., \"01\") */\n series: Record<string, string[]>;\n};\n\n/**\n * Create a new empty state (v3.0.0)\n */\nexport function createEmptyState(): State {\n return {\n version: '3.0.0',\n series: {},\n };\n}\n","/**\n * Global application context\n *\n * Provides centralized access to shared services (config, notifier).\n * Eliminates the need to pass these dependencies through multiple layers.\n *\n * Usage:\n * 1. Initialize early in app startup: AppContext.initialize(...)\n * 2. Access anywhere: import { AppContext } from './app-context'\n */\n\nimport type { ConfigRegistry } from './config/config-registry.js';\nimport type { Notifier } from './notifications/notifier.js';\nimport { StateManager } from './state/state-manager.js';\n\n/**\n * Global application context singleton\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: Intentional singleton pattern for global app context\nexport class AppContext {\n private static configRegistry?: ConfigRegistry;\n private static notifier?: Notifier;\n private static stateManager?: StateManager;\n\n /**\n * Initialize the application context with pre-created services\n *\n * Called once during app startup to set up shared services.\n *\n * @param configRegistry - Config registry instance\n * @param notifier - Notifier instance\n * @param stateManager - State manager instance (optional, created from notifier if not provided)\n */\n static initialize(configRegistry: ConfigRegistry, notifier: Notifier, stateManager?: StateManager): void {\n AppContext.configRegistry = configRegistry;\n AppContext.notifier = notifier;\n AppContext.stateManager = stateManager || (notifier ? new StateManager(notifier) : undefined);\n }\n\n /**\n * Get the config registry instance\n *\n * @throws Error if context not initialized\n */\n static getConfig(): ConfigRegistry {\n if (!AppContext.configRegistry) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.configRegistry;\n }\n\n /**\n * Get the notifier instance\n *\n * @throws Error if context not initialized\n */\n static getNotifier(): Notifier {\n if (!AppContext.notifier) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.notifier;\n }\n\n /**\n * Get the state manager instance\n *\n * @throws Error if context not initialized\n */\n static getStateManager(): StateManager {\n if (!AppContext.stateManager) {\n throw new Error('AppContext not initialized. Call AppContext.initialize() first.');\n }\n return AppContext.stateManager;\n }\n\n /**\n * Reload configuration\n *\n * Updates the ConfigRegistry with new configuration.\n * Useful for runtime config reloading.\n *\n * @param configRegistry - New config registry instance\n */\n static reloadConfig(configRegistry: ConfigRegistry): void {\n AppContext.configRegistry = configRegistry;\n }\n\n /**\n * Update the notifier instance\n *\n * Useful for hot-swapping notifiers (e.g., adding Telegram).\n *\n * @param notifier - New notifier instance\n */\n static setNotifier(notifier: Notifier): void {\n AppContext.notifier = notifier;\n }\n\n /**\n * Check if context is initialized\n */\n static isInitialized(): boolean {\n return AppContext.configRegistry !== undefined;\n }\n\n /**\n * Reset the context (useful for testing)\n */\n static reset(): void {\n AppContext.configRegistry = undefined;\n AppContext.notifier = undefined;\n AppContext.stateManager = undefined;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as yaml from 'js-yaml';\nimport type { Config, RawConfig } from '../config/config-schema.js';\nimport { ConfigError } from '../errors/custom-errors';\nimport { resolveEnvRecursive } from '../utils/env-resolver';\nimport { validateConfigWithWarnings } from './config-schema';\n\n/**\n * Default config file path\n */\nexport const DEFAULT_CONFIG_PATH = './config.yaml';\n\n/**\n * Load and parse configuration from YAML file\n *\n * @param configPath - Path to config file\n * @returns Parsed configuration\n * @throws ConfigError if file doesn't exist or is invalid\n */\nexport async function loadConfig(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n // Resolve relative path\n const absolutePath = join(process.cwd(), configPath);\n\n if (!existsSync(absolutePath)) {\n throw new ConfigError(\n `Configuration file not found: \"${absolutePath}\". Create a config.yaml file or specify a different path.`,\n );\n }\n\n const content = await readFile(absolutePath, 'utf-8');\n\n let rawConfig: RawConfig;\n\n try {\n rawConfig = yaml.load(content) as RawConfig;\n } catch (error) {\n throw new ConfigError(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n // Validate configuration structure and check for common mistakes\n validateConfigWithWarnings(rawConfig);\n\n // Resolve environment variables\n const config = resolveEnvRecursive(rawConfig) as unknown as Config;\n\n return config;\n}\n\n/**\n * Load config with defaults for optional fields\n */\nexport async function loadConfigWithDefaults(configPath: string = DEFAULT_CONFIG_PATH): Promise<Config> {\n return loadConfig(configPath);\n}\n","/**\n * Base error class for wetvlo\n */\nexport class WetvloError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'WetvloError';\n }\n}\n\n/**\n * Configuration error\n */\nexport class ConfigError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * State file error\n */\nexport class StateError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'StateError';\n }\n}\n\n/**\n * Handler error (episode extraction issues)\n */\nexport class HandlerError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'HandlerError';\n }\n}\n\n/**\n * Download error\n */\nexport class DownloadError extends WetvloError {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message);\n this.name = 'DownloadError';\n }\n}\n\n/**\n * Notification error\n */\nexport class NotificationError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'NotificationError';\n }\n}\n\n/**\n * Cookie extraction error\n */\nexport class CookieError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'CookieError';\n }\n}\n\n/**\n * Scheduling error\n */\nexport class SchedulerError extends WetvloError {\n constructor(message: string) {\n super(message);\n this.name = 'SchedulerError';\n }\n}\n","/**\n * Resolve environment variables in strings\n * Supports ${VAR_NAME} syntax\n *\n * @param value - String that may contain ${VAR_NAME} placeholders\n * @returns String with environment variables resolved\n */\nexport function resolveEnv(value: string): string {\n if (typeof value !== 'string') {\n return value;\n }\n\n return value.replace(/\\$\\{([^}]+)\\}/g, (_match, varName) => {\n const envValue = process.env[varName];\n if (envValue === undefined) {\n throw new Error(`Environment variable \"${varName}\" is not set`);\n }\n return envValue;\n });\n}\n\n/**\n * Recursively resolve environment variables in object\n *\n * @param obj - Object that may contain strings with ${VAR_NAME}\n * @returns Object with all environment variables resolved\n */\nexport function resolveEnvRecursive<T>(obj: T): T {\n if (typeof obj === 'string') {\n return resolveEnv(obj) as T;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => resolveEnvRecursive(item)) as T;\n }\n\n if (obj !== null && typeof obj === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[key] = resolveEnvRecursive(value);\n }\n return result as T;\n }\n\n return obj;\n}\n","/**\n * Zod schemas for configuration validation\n *\n * This file defines both the validation schemas AND the TypeScript types.\n * Types are automatically inferred from the schemas, ensuring they stay in sync.\n */\n\nimport { z } from 'zod';\nimport type { DeepMerge } from '../utils/deep-merge';\nimport type { DefaultConfig } from './config-defaults';\n\n/**\n * Episode types\n */\nconst EpisodeTypeSchema = z.enum(['available', 'vip', 'svip', 'teaser', 'express', 'preview', 'locked']);\n\nexport type EpisodeType = z.infer<typeof EpisodeTypeSchema>;\n/**\n * Check settings for series/domain\n */\nexport const CheckSettingsSchema = z.object({\n count: z.number().positive().optional().describe('Number of episodes to check'),\n checkInterval: z.number().positive().optional().describe('Interval between checks in seconds'),\n downloadTypes: z.array(EpisodeTypeSchema).optional().describe('Episode types to download'),\n});\n\nexport type CheckSettings = z.infer<typeof CheckSettingsSchema>;\n\nexport type CheckSettingsResolved = DeepMerge<DefaultConfig['check'], CheckSettings>;\n/**\n * Download settings for series/domain\n */\nexport const DownloadSettingsSchema = z.object({\n downloadDir: z.string().optional().describe('Directory to save downloaded episodes'),\n tempDir: z.string().optional().describe('Directory for temporary files'),\n downloadDelay: z.number().nonnegative().optional().describe('Delay between downloads in milliseconds'),\n maxRetries: z.number().int().nonnegative().optional().describe('Maximum number of retry attempts'),\n initialTimeout: z.number().positive().optional().describe('Initial timeout for operations in milliseconds'),\n backoffMultiplier: z.number().positive().optional().describe('Multiplier for exponential backoff'),\n jitterPercentage: z.number().int().min(0).max(100).optional().describe('Jitter percentage for retry delays'),\n minDuration: z.number().nonnegative().optional().describe('Minimum duration in seconds for downloads'),\n});\n\nexport type DownloadSettings = z.infer<typeof DownloadSettingsSchema>;\n\nexport type DownloadSettingsResolved = DeepMerge<DefaultConfig['download'], DownloadSettings>;\n\n/**\n * Telegram notification configuration\n */\nexport const TelegramConfigSchema = z.object({\n botToken: z.string().describe('Telegram bot token'),\n chatId: z.string().describe('Telegram chat ID'),\n});\n\nexport type TelegramConfig = z.infer<typeof TelegramConfigSchema>;\n\n/**\n * Browser options\n */\nconst BrowserSchema = z.enum(['chrome', 'firefox', 'safari', 'chromium', 'edge']);\n\nconst CommonSettingsSchema = z.object({\n check: CheckSettingsSchema.optional().describe('Check settings'),\n download: DownloadSettingsSchema.optional().describe('Download settings'),\n telegram: TelegramConfigSchema.optional().describe('Telegram notification configuration'),\n stateFile: z.string().optional().describe('Path to state file'),\n browser: BrowserSchema.optional().describe('Browser to use for scraping'),\n cookieFile: z.string().optional().describe('Path to cookie file'),\n subLangs: z.array(z.string()).optional().describe('List of subtitle languages to download'),\n});\n\n/**\n * Global configuration defaults\n */\nexport const GlobalConfigSchema = CommonSettingsSchema;\n\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n\nexport type GlobalConfigResolved = DeepMerge<DefaultConfig, GlobalConfig>;\n\n/**\n * Domain-specific configuration\n */\nexport const DomainConfigSchema = CommonSettingsSchema.extend({\n domain: z.string().describe('Domain name (e.g., \"weTV\")'),\n});\n\nexport type DomainConfig = z.infer<typeof DomainConfigSchema>;\n\nexport type DomainConfigResolved = DeepMerge<GlobalConfigResolved, DomainConfig>;\n\n/**\n * Series configuration\n */\nexport const SeriesConfigSchema = CommonSettingsSchema.extend({\n name: z.string().describe('Series name'),\n url: z.url().describe('Series URL'),\n startTime: z\n .string()\n .regex(/^\\d{1,2}:\\d{2}$/, {\n message: 'Must be in HH:MM format (e.g., \"20:00\")',\n })\n .optional()\n .describe('Start time in HH:MM format'),\n cron: z.string().optional().describe('Cron expression for scheduling'),\n});\n\nexport type SeriesConfig = z.infer<typeof SeriesConfigSchema>;\n\nexport type SeriesConfigResolved = DeepMerge<DomainConfigResolved, SeriesConfig>;\n\n/**\n * Main configuration schema\n */\nexport const ConfigSchema = z.object({\n series: z.array(SeriesConfigSchema).min(1, 'Cannot be empty').describe('List of series to monitor'),\n domainConfigs: z.array(DomainConfigSchema).optional().describe('Domain-specific configurations'),\n globalConfig: GlobalConfigSchema.optional().describe('Global configuration defaults'),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Raw configuration before env var resolution\n */\nexport type RawConfig = Record<string, unknown>;\n\n/**\n * Configuration level for resolution\n */\nexport type Level = 'global' | 'domain' | 'series';\n\n/**\n * Resolved configuration type based on level\n */\nexport type ResolvedConfig<L extends Level> = L extends 'global'\n ? GlobalConfigResolved\n : L extends 'domain'\n ? DomainConfigResolved\n : SeriesConfigResolved;\n\n/**\n * Validate configuration and check for common mistakes\n *\n * @param rawConfig - Raw configuration object from YAML\n * @throws ConfigError if validation fails or common mistakes are found\n */\nexport function validateConfigWithWarnings(rawConfig: RawConfig): void {\n // First, do the basic validation\n ConfigSchema.parse(rawConfig);\n\n // Then check for common configuration mistakes\n const warnings: string[] = [];\n\n // Check for misplaced download settings\n if (rawConfig.globalConfig) {\n const globalConfig = rawConfig.globalConfig as any;\n\n // Check if downloadDir or tempDir are directly under globalConfig\n if (globalConfig.downloadDir && !globalConfig.download?.downloadDir) {\n warnings.push(\n `'downloadDir' found directly under 'globalConfig'. ` +\n `It should be placed under 'globalConfig.download'. ` +\n `Current value: \"${globalConfig.downloadDir}\"`,\n );\n }\n\n if (globalConfig.tempDir && !globalConfig.download?.tempDir) {\n warnings.push(\n `'tempDir' found directly under 'globalConfig'. ` +\n `It should be placed under 'globalConfig.download'. ` +\n `Current value: \"${globalConfig.tempDir}\"`,\n );\n }\n\n // Check for misplaced check settings\n if (globalConfig.count && !globalConfig.check?.count) {\n warnings.push(\n `'count' found directly under 'globalConfig'. ` + `It should be placed under 'globalConfig.check'.`,\n );\n }\n\n if (globalConfig.checkInterval && !globalConfig.check?.checkInterval) {\n warnings.push(\n `'checkInterval' found directly under 'globalConfig'. ` + `It should be placed under 'globalConfig.check'.`,\n );\n }\n }\n\n // Check for common typo: globalConfigs instead of globalConfig\n if ((rawConfig as any).globalConfigs) {\n warnings.push(`'globalConfigs' found. Did you mean 'globalConfig'?`);\n }\n\n // Check for misplaced settings in domain configs\n if (rawConfig.domainConfigs && Array.isArray(rawConfig.domainConfigs)) {\n rawConfig.domainConfigs.forEach((domainConfig: any, index: number) => {\n const dc = domainConfig as any;\n\n if (dc.downloadDir && !dc.download?.downloadDir) {\n warnings.push(\n `'downloadDir' found directly under 'domainConfigs[${index}]. ` +\n `It should be placed under 'domainConfigs[${index}].download'.`,\n );\n }\n });\n }\n\n // If there are warnings, log them\n if (warnings.length > 0) {\n console.warn('\\n⚠️ Configuration Warnings:');\n console.warn('The following configuration issues were detected:');\n warnings.forEach((warning, index) => {\n console.warn(`${index + 1}. ${warning}`);\n });\n console.warn('Please fix these issues in your config.yaml file.\\n');\n }\n}\n\n/**\n * Validate configuration using Zod\n *\n * @param rawConfig - Raw configuration object from YAML\n * @throws z.ZodError if validation fails\n */\nexport function validateConfig(rawConfig: RawConfig): void {\n ConfigSchema.parse(rawConfig);\n}\n\n/**\n * Validate with custom error formatting\n *\n * @param rawConfig - Raw configuration object\n * @returns Object with { success: boolean, error?: string }\n */\nexport function validateConfigSafe(rawConfig: RawConfig): { success: true } | { success: false; error: string } {\n try {\n ConfigSchema.parse(rawConfig);\n return { success: true };\n } catch (error) {\n if (error instanceof z.ZodError) {\n return { success: false, error: formatZodError(error) };\n }\n return { success: false, error: String(error) };\n }\n}\n\n/**\n * Format Zod error into a readable message\n */\nfunction formatZodError(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? `\"${issue.path.join('.')}\"` : 'value';\n const code = issue.code.toUpperCase();\n return `${path} ${issue.message} [${code}]`;\n })\n .join('; ');\n}\n","export function deepMerge<T extends object, U extends object>(target: T, source?: U): DeepMerge<T, U> {\n if (!source) {\n return target as unknown as DeepMerge<T, U>;\n }\n\n const result = { ...target } as Record<string, unknown>;\n\n for (const key in source) {\n if (Object.hasOwn(source, key)) {\n const sourceValue = source[key];\n const targetValue = result[key];\n\n if (isObject(sourceValue) && isObject(targetValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n }\n\n return result as DeepMerge<T, U>;\n}\n\nfunction isObject(item: unknown): item is object {\n return typeof item === 'object' && item !== null && !Array.isArray(item);\n}\n\n// 1. Utility for forced type disclosure (nice output in IDE)\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n// 2. Smart check for object/Record\n// Exclude arrays and functions, consider the possibility of undefined\n// biome-ignore lint/complexity/noBannedTypes: type check\n// biome-ignore lint/suspicious/noExplicitAny: type check\ntype IsRecord<T> = T extends object ? (T extends any[] ? false : T extends Function ? false : true) : false;\n\n// Helper for obtaining a pure type without undefined\ntype NotUndefined<T> = Exclude<T, undefined>;\n\n// 3. Choosing keys\ntype OptionalKeys<T> = {\n // biome-ignore lint/complexity/noBannedTypes: type check\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\n\n// 4. Merge values\ntype MergeValues<T, S> =\n // Check if both values (without undefined) are objects\n IsRecord<NotUndefined<T>> extends true\n ? IsRecord<NotUndefined<S>> extends true\n ? // IF BOTH ARE OBJECTS:\n // Recursively merge their \"clean\" versions.\n // IMPORTANT: We removed `| (undefined extends S ? T : never)`,\n // to prevent the strict type from Source from being swallowed by the weak type from Target.\n DeepMerge<NotUndefined<T>, NotUndefined<S>>\n : // IF DIFFERENT TYPES (or primitives):\n SimpleMerge<T, S>\n : SimpleMerge<T, S>;\n\n// 5. Simple merge for primitives\n// Here we leave the fallback to T, as it's safe for primitives (string | undefined)\ntype SimpleMerge<T, S> = NotUndefined<S> | (undefined extends S ? T : never);\n\n// 6. Main type DeepMerge\nexport type DeepMerge<T, S> =\n IsRecord<NotUndefined<T>> extends true\n ? IsRecord<NotUndefined<S>> extends true\n ? Simplify<\n // Keys from T (that are not in S)\n Pick<T, Exclude<keyof T, keyof S>> &\n // Keys from S (that are not in T)\n Pick<S, Exclude<keyof S, keyof T>> & {\n // If a key is required in AT LEAST ONE object -> it's required // Common keys:\n [K in (RequiredKeys<T> & keyof S) | (RequiredKeys<S> & keyof T)]: MergeValues<T[K], S[K]>;\n // biome-ignore lint/suspicious/noExplicitAny: type check\n } & { [K in (OptionalKeys<T> & OptionalKeys<S>) & keyof any]?: MergeValues<T[K], S[K]> } // If a key is optional in BOTH -> it's optional\n >\n : S // If S is no longer an object, it overwrites T\n : S;\n","/**\n * Extract domain from URL\n *\n * @param url - URL to extract domain from\n * @returns Domain (e.g., \"wetv.vip\")\n */\nexport function extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n throw new Error(`Invalid URL: \"${url}\"`);\n }\n}\n\n/**\n * Check if URL is valid\n *\n * @param url - URL to validate\n * @returns True if URL is valid\n */\nexport function isValidUrl(url: string): boolean {\n try {\n new URL(url);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Normalize URL by removing trailing slash and fragment\n *\n * @param url - URL to normalize\n * @returns Normalized URL\n */\nexport function normalizeUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n urlObj.hash = '';\n // Remove trailing slash from pathname\n if (urlObj.pathname.endsWith('/')) {\n urlObj.pathname = urlObj.pathname.slice(0, -1);\n }\n return urlObj.toString();\n } catch {\n return url;\n }\n}\n","type EpisodeType = 'available' | 'vip' | 'svip' | 'teaser' | 'express' | 'preview' | 'locked';\n\nexport type DefaultConfig = {\n stateFile: string;\n browser?: string;\n cookieFile?: string;\n\n check: {\n count: number;\n checkInterval: number;\n downloadTypes: EpisodeType[];\n };\n\n download: {\n downloadDir: string;\n tempDir: string;\n downloadDelay: number;\n maxRetries: number;\n initialTimeout: number;\n backoffMultiplier: number;\n jitterPercentage: number;\n minDuration: number;\n };\n\n telegram?: {\n botToken: string;\n chatId: string;\n };\n};\n\nexport const defaults: DefaultConfig = {\n check: {\n count: 3,\n checkInterval: 600,\n downloadTypes: ['available'],\n },\n download: {\n downloadDir: './downloads',\n tempDir: './downloads',\n downloadDelay: 10,\n maxRetries: 3,\n initialTimeout: 5,\n backoffMultiplier: 2,\n jitterPercentage: 10,\n minDuration: 0,\n },\n stateFile: 'wetvlo-state.json',\n browser: 'chrome',\n};\n\n/**\n * Default configuration values\n */\nexport function getDefaults() {\n return defaults;\n}\n","/**\n * ConfigRegistry - Centralized configuration registry with pre-merged configs\n *\n * Merges configuration at construction time:\n * defaults → global → domain → series\n *\n * Simplified API:\n * - registry.resolve(url) - Get resolved config for a series URL\n * - registry.resolve(url, \"domain\") - Get domain-level config\n * - registry.resolve(url, \"global\") - Get global-level config\n */\n\nimport { deepMerge } from '../utils/deep-merge.js';\nimport { extractDomain } from '../utils/url-utils.js';\nimport { getDefaults } from './config-defaults.js';\nimport type {\n Config,\n DomainConfigResolved,\n GlobalConfigResolved,\n Level,\n ResolvedConfig,\n SeriesConfigResolved,\n} from './config-schema.js';\n\ntype SeriesKey = `series:${string}`;\n\ntype DomainKye = `domain:${string}`;\n\ntype GlobalKey = 'global';\n\ntype ValidKey = GlobalKey | DomainKye | SeriesKey;\n\n/**\n * Configuration registry with pre-merged configs\n */\nexport class ConfigRegistry {\n private readonly map = new Map<ValidKey, GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved>();\n private readonly seriesByUrl = new Map<string, SeriesConfigResolved>();\n\n /**\n * Create a new ConfigRegistry\n *\n * @param root - Root configuration object\n */\n constructor(root: Config) {\n // Merge at construction time: defaults → global → domain → series\n const defaults = getDefaults();\n\n const globalMerged = deepMerge(defaults, root.globalConfig);\n this.setConfig('global', globalMerged);\n\n // Domain configs\n for (const dc of root.domainConfigs || []) {\n const domainMerged = deepMerge(globalMerged, dc);\n this.setConfig(`domain:${dc.domain}`, domainMerged);\n }\n\n // Series configs\n for (const sc of root.series) {\n const hostname = extractDomain(sc.url);\n let domainMerged = this.getConfig(`domain:${hostname}`);\n if (!domainMerged) {\n const globalConfig = this.getConfig('global');\n domainMerged = deepMerge(globalConfig, { domain: hostname });\n }\n const seriesMerged = deepMerge(domainMerged, sc);\n this.setConfig(`series:${sc.url}`, seriesMerged);\n this.seriesByUrl.set(sc.url, seriesMerged);\n }\n }\n\n getConfig(key: 'global'): GlobalConfigResolved;\n getConfig(key: `domain:${string}`): DomainConfigResolved | undefined;\n getConfig(key: `series:${string}`): SeriesConfigResolved;\n getConfig(key: ValidKey): GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved | undefined {\n return this.map.get(key) as GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved | undefined;\n }\n\n setConfig(key: 'global', config: GlobalConfigResolved): void;\n setConfig(key: `domain:${string}`, config: DomainConfigResolved): void;\n setConfig(key: `series:${string}`, config: SeriesConfigResolved): void;\n setConfig(key: ValidKey, config: GlobalConfigResolved | DomainConfigResolved | SeriesConfigResolved): void {\n this.map.set(key, config);\n }\n\n /**\n * Resolve configuration for a URL\n *\n * @param url - Series URL\n * @param level - Resolution level (\"full\", \"domain\", or \"global\")\n * @returns Resolved configuration\n */\n resolve<L extends Level>(url: string, level?: L): ResolvedConfig<L> {\n if (level === 'global') {\n const config = this.getConfig('global');\n if (!config) {\n throw new Error('Global configuration not found');\n }\n return config as ResolvedConfig<L>;\n }\n\n if (level === 'domain') {\n const domain = extractDomain(url);\n const config = this.getConfig(`domain:${domain}`);\n if (!config) {\n // Fall back to global if domain config not found\n const globalConfig = this.getConfig('global');\n if (!globalConfig) {\n throw new Error('Global configuration not found');\n }\n return Object.assign(globalConfig, { domain }) as ResolvedConfig<L>;\n }\n return config as ResolvedConfig<L>;\n }\n\n // Default to \"full\" resolution\n const config = this.getConfig(`series:${url}`);\n if (!config) {\n throw new Error(`No configuration found for URL: ${url}`);\n }\n\n const resolved = config;\n this.validate(resolved);\n return resolved as ResolvedConfig<L>;\n }\n\n /**\n * List all series configurations\n *\n * @returns Array of series configurations\n */\n listSeries(): SeriesConfigResolved[] {\n return Array.from(this.seriesByUrl.values());\n }\n\n /**\n * List all series URLs\n *\n * @returns Array of series URLs\n */\n listSeriesUrls(): string[] {\n return Array.from(this.seriesByUrl.keys());\n }\n\n /**\n * List all configured domains\n *\n * @returns Array of domain names\n */\n listDomains(): string[] {\n const domains = new Set<string>();\n for (const url of this.seriesByUrl.keys()) {\n domains.add(extractDomain(url));\n }\n return Array.from(domains);\n }\n\n /**\n * Validate resolved configuration\n */\n private validate(config: SeriesConfigResolved): void {\n if (!config.check) {\n throw new Error('Missing check configuration');\n }\n if (!config.download) {\n throw new Error('Missing download configuration');\n }\n\n const { check, download } = config;\n\n if (check.count < 1) {\n throw new Error(`Invalid check count: ${check.count}`);\n }\n if (check.checkInterval < 0) {\n throw new Error(`Invalid check interval: ${check.checkInterval}`);\n }\n if (download.downloadDelay < 0) {\n throw new Error(`Invalid download delay: ${download.downloadDelay}`);\n }\n if (download.maxRetries < 0) {\n throw new Error(`Invalid max retries: ${download.maxRetries}`);\n }\n if (download.initialTimeout < 0) {\n throw new Error(`Invalid initial timeout: ${download.initialTimeout}`);\n }\n if (download.backoffMultiplier < 1) {\n throw new Error(`Invalid backoff multiplier: ${download.backoffMultiplier}`);\n }\n if (download.minDuration < 0) {\n throw new Error(`Invalid min duration: ${download.minDuration}`);\n }\n }\n}\n","import * as fs from 'node:fs';\nimport * as fsPromises from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport { AppContext } from '../app-context.js';\nimport { DownloadError } from '../errors/custom-errors';\nimport { NotificationLevel } from '../notifications/notifier';\nimport type { StateManager } from '../state/state-manager';\nimport type { Episode } from '../types/episode.types';\nimport { sanitizeFilename } from '../utils/filename-sanitizer';\nimport * as VideoValidator from '../utils/video-validator';\nimport type { DownloadOptions } from './download-options.js';\nimport { extractDownloadOptions } from './download-options.js';\nimport { downloaderRegistry } from './downloader-registry';\nimport { YtDlpDownloader } from './impl/yt-dlp-downloader';\n\n/**\n * Escape special characters in a string for use in a regular expression\n */\nfunction escapeRegExp(string: string): string {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Download manager with progress tracking\n */\nexport class DownloadManager {\n private stateManager: StateManager;\n\n constructor() {\n // Get StateManager from AppContext\n this.stateManager = AppContext.getStateManager();\n }\n\n /**\n * Download an episode using appropriate downloader\n */\n async download(seriesUrl: string, episode: Episode): Promise<boolean> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n\n // Get resolved config\n const resolved = registry.resolve(seriesUrl, 'series');\n const statePath = resolved.stateFile;\n const seriesName = resolved.name;\n const downloadOptions: DownloadOptions = extractDownloadOptions(resolved);\n\n // Check if already downloaded\n if (this.stateManager.isDownloaded(statePath, seriesName, episode.number)) {\n return false;\n }\n\n const downloader = downloaderRegistry.getDownloader(episode.url);\n notifier.notify(\n NotificationLevel.HIGHLIGHT,\n `Downloading Episode ${episode.number} of ${seriesName} using ${downloader.getName()}`,\n );\n\n // Calculate filename once (used in both try and catch)\n const paddedNumber = String(episode.number).padStart(2, '0');\n const sanitizedSeriesName = sanitizeFilename(seriesName);\n const filenameWithoutExt = `${sanitizedSeriesName} - ${paddedNumber}`;\n const targetDir = downloadOptions.tempDir || downloadOptions.downloadDir;\n\n try {\n // Clean up any artifacts from previous failed attempts\n await this.cleanupEpisodeArtifacts(targetDir, filenameWithoutExt);\n\n const result = await downloader.download(episode, targetDir, filenameWithoutExt, {\n cookieFile: downloadOptions.cookieFile,\n subLangs: downloadOptions.subLangs,\n onProgress: (progress) => notifier.progress(progress),\n onLog: (message) => notifier.notify(NotificationLevel.INFO, message),\n });\n\n // End progress display (add newline)\n notifier.endProgress();\n\n // Verify file exists and has size\n const fileSize = this.verifyDownload(result.filename);\n\n if (fileSize === 0) {\n await this.cleanupFiles(result.allFiles);\n throw new Error('Downloaded file is empty or does not exist');\n }\n\n // Verify duration if required\n if (downloadOptions.minDuration > 0) {\n const fullPath = resolve(result.filename);\n const duration = await VideoValidator.getVideoDuration(fullPath);\n if (duration < downloadOptions.minDuration) {\n // Delete all downloaded files\n await this.cleanupFiles(result.allFiles);\n throw new Error(`Video duration ${duration}s is less than minimum ${downloadOptions.minDuration}s`);\n }\n }\n\n // Move files from tempDir to downloadDir if needed\n if (downloadOptions.tempDir && downloadOptions.tempDir !== downloadOptions.downloadDir) {\n notifier.notify(\n NotificationLevel.INFO,\n `Moving files from temp directory to ${downloadOptions.downloadDir}...`,\n );\n\n // Ensure download directory exists\n await fsPromises.mkdir(downloadOptions.downloadDir, { recursive: true });\n\n for (const file of result.allFiles) {\n try {\n // Resolve 'file' to absolute path just in case\n const absFile = resolve(file);\n\n if (!fs.existsSync(absFile)) {\n notifier.notify(NotificationLevel.WARNING, `File not found, skipping move: ${absFile}`);\n continue;\n }\n\n const fileName = basename(absFile);\n const newPath = join(downloadOptions.downloadDir, fileName);\n await fsPromises.rename(absFile, newPath);\n\n // Update filename if it matches the main file\n if (absFile === resolve(result.filename)) {\n result.filename = newPath;\n }\n } catch (e) {\n notifier.notify(NotificationLevel.ERROR, `Failed to move file ${file}: ${e}`);\n }\n }\n }\n\n // Add to state\n await this.stateManager.addDownloadedEpisode(statePath, seriesName, episode.number);\n\n notifier.notify(\n NotificationLevel.SUCCESS,\n `Downloaded Episode ${episode.number}: ${result.filename} (${this.formatSize(fileSize)})`,\n );\n\n return true;\n } catch (error) {\n // End progress display on error\n notifier.endProgress();\n\n // Clean up any artifacts from this failed attempt\n await this.cleanupEpisodeArtifacts(targetDir, filenameWithoutExt);\n\n const message = `Failed to download Episode ${episode.number}: ${\n error instanceof Error ? error.message : String(error)\n }`;\n\n notifier.notify(NotificationLevel.ERROR, message);\n throw new DownloadError(message, episode.url);\n }\n }\n\n /**\n * Clean up downloaded files\n */\n private async cleanupFiles(files: string[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n for (const file of files) {\n try {\n const fullPath = resolve(file);\n if (fs.existsSync(fullPath)) {\n await fsPromises.unlink(fullPath);\n }\n } catch (e) {\n notifier.notify(NotificationLevel.ERROR, `Failed to delete file ${file}: ${e}`);\n }\n }\n }\n\n /**\n * Clean up all files matching episode pattern (artifacts from failed downloads)\n *\n * @param dir - Directory to clean (tempDir or downloadDir)\n * @param filenameWithoutExt - Episode filename without extension (e.g., \"SeriesName - 01\")\n */\n private async cleanupEpisodeArtifacts(dir: string, filenameWithoutExt: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n try {\n const absDir = resolve(dir);\n if (!fs.existsSync(absDir)) {\n return; // Directory doesn't exist, nothing to clean\n }\n\n const files = await fsPromises.readdir(absDir);\n const pattern = new RegExp(`^${escapeRegExp(filenameWithoutExt)}\\\\..*$`);\n\n let cleanedCount = 0;\n for (const file of files) {\n if (pattern.test(file)) {\n const filePath = join(absDir, file);\n try {\n await fsPromises.unlink(filePath);\n cleanedCount++;\n notifier.notify(NotificationLevel.INFO, `Cleaned up artifact: ${file}`);\n } catch (e) {\n notifier.notify(NotificationLevel.WARNING, `Failed to delete artifact ${file}: ${e}`);\n }\n }\n }\n\n if (cleanedCount > 0) {\n notifier.notify(NotificationLevel.INFO, `Cleaned up ${cleanedCount} artifact(s) for ${filenameWithoutExt}`);\n }\n } catch (e) {\n notifier.notify(NotificationLevel.WARNING, `Failed to cleanup artifacts in ${dir}: ${e}`);\n }\n }\n\n /**\n * Verify downloaded file exists and get its size\n */\n private verifyDownload(filename: string): number {\n const fullPath = resolve(filename);\n\n try {\n const stats = fs.statSync(fullPath);\n return stats.size;\n } catch {\n return 0;\n }\n }\n\n /**\n * Format file size for display\n */\n private formatSize(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let size = bytes;\n let unit = 0;\n\n while (size >= 1024 && unit < units.length - 1) {\n size /= 1024;\n unit++;\n }\n\n return `${size.toFixed(2)} ${units[unit]}`;\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkYtDlpInstalled(): Promise<boolean> {\n return YtDlpDownloader.checkInstalled();\n }\n}\n","/**\n * Utility to sanitize filenames for cross-platform compatibility\n * Specifically targets Windows restrictions which are stricter than *nix\n */\nexport function sanitizeFilename(name: string): string {\n return (\n name\n // Replace Windows illegal characters: < > : \" / \\ | ? *\n .replace(/[<>:\"/\\\\|?*]/g, '_')\n // Remove control characters (0-31 in ASCII)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: Needed to strip control characters\n .replace(/[\\x00-\\x1F]/g, '')\n // Remove trailing spaces and dots (Windows doesn't like them)\n .replace(/[\\s.]+$/, '')\n );\n}\n","import { execa } from 'execa';\nimport { logger } from './logger';\n\n/**\n * Utility to validate video files\n */\n\n/**\n * Get video duration in seconds using ffprobe\n *\n * @param filePath - Path to video file\n * @returns Duration in seconds, or 0 if failed\n */\nexport async function getVideoDuration(filePath: string): Promise<number> {\n try {\n // ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4\n const { stdout } = await execa('ffprobe', [\n '-v',\n 'error',\n '-show_entries',\n 'format=duration',\n '-of',\n 'default=noprint_wrappers=1:nokey=1',\n filePath,\n ]);\n\n const duration = parseFloat(stdout.trim());\n return Number.isNaN(duration) ? 0 : duration;\n } catch (error) {\n logger.error(\n `Failed to get video duration for ${filePath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n return 0;\n }\n}\n\n/**\n * Check if ffprobe is installed\n */\nexport async function checkFfprobeInstalled(): Promise<boolean> {\n try {\n await execa('ffprobe', ['-version']);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Log level\n */\nexport enum LogLevel {\n DEBUG = 'DEBUG',\n INFO = 'INFO',\n SUCCESS = 'SUCCESS',\n WARNING = 'WARNING',\n ERROR = 'ERROR',\n HIGHLIGHT = 'HIGHLIGHT',\n}\n\n/**\n * Logger configuration\n */\nexport type LoggerConfig = {\n level: LogLevel;\n useColors: boolean;\n};\n\n/**\n * ANSI color codes\n */\nconst colors = {\n reset: '\\x1b[0m',\n bright: '\\x1b[1m',\n dim: '\\x1b[2m',\n\n // Foreground colors\n black: '\\x1b[30m',\n red: '\\x1b[31m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n blue: '\\x1b[34m',\n magenta: '\\x1b[35m',\n cyan: '\\x1b[36m',\n white: '\\x1b[37m',\n\n // Background colors\n bgRed: '\\x1b[41m',\n bgGreen: '\\x1b[42m',\n bgYellow: '\\x1b[43m',\n};\n\n/**\n * Logger class with colored console output\n */\nexport class Logger {\n private config: LoggerConfig;\n\n constructor(config: Partial<LoggerConfig> = {}) {\n this.config = {\n level: config.level ?? LogLevel.INFO,\n useColors: config.useColors ?? true,\n };\n }\n\n /**\n * Get emoji for log level\n */\n private getEmoji(level: LogLevel): string {\n switch (level) {\n case LogLevel.DEBUG:\n return '🔍';\n case LogLevel.INFO:\n return 'ℹ️';\n case LogLevel.SUCCESS:\n return '✅';\n case LogLevel.WARNING:\n return '⚠️';\n case LogLevel.ERROR:\n return '❌';\n case LogLevel.HIGHLIGHT:\n return '🌟';\n default:\n return '•';\n }\n }\n\n /**\n * Format date to human readable string (MM-DD HH:mm:ss)\n */\n private formatDate(date: Date): string {\n const month = (date.getMonth() + 1).toString().padStart(2, '0');\n const day = date.getDate().toString().padStart(2, '0');\n const hour = date.getHours().toString().padStart(2, '0');\n const min = date.getMinutes().toString().padStart(2, '0');\n const sec = date.getSeconds().toString().padStart(2, '0');\n return `${month}-${day} ${hour}:${min}:${sec}`;\n }\n\n /**\n * Format log message with timestamp and level\n */\n private format(level: LogLevel, message: string): string {\n const timestamp = this.formatDate(new Date());\n const emoji = this.getEmoji(level);\n return `${timestamp} ${emoji} ${message}`;\n }\n\n /**\n * Apply color to text\n */\n private colorize(text: string, color: string): string {\n if (!this.config.useColors) return text;\n return `${color}${text}${colors.reset}`;\n }\n\n /**\n * Log debug message\n */\n debug(message: string): void {\n if (this.shouldLog(LogLevel.DEBUG)) {\n console.log(this.format(LogLevel.DEBUG, this.colorize(message, colors.dim)));\n }\n }\n\n /**\n * Log info message\n */\n info(message: string): void {\n if (this.shouldLog(LogLevel.INFO)) {\n console.log(this.format(LogLevel.INFO, this.colorize(message, colors.dim + colors.white)));\n }\n }\n\n /**\n * Log success message\n */\n success(message: string): void {\n if (this.shouldLog(LogLevel.SUCCESS)) {\n console.log(this.format(LogLevel.SUCCESS, this.colorize(message, colors.green)));\n }\n }\n\n /**\n * Log warning message\n */\n warning(message: string): void {\n if (this.shouldLog(LogLevel.WARNING)) {\n console.log(this.format(LogLevel.WARNING, this.colorize(message, colors.yellow)));\n }\n }\n\n /**\n * Log error message\n */\n error(message: string): void {\n if (this.shouldLog(LogLevel.ERROR)) {\n console.error(this.format(LogLevel.ERROR, this.colorize(message, colors.red)));\n }\n }\n\n /**\n * Log highlighted message\n */\n highlight(message: string): void {\n if (this.shouldLog(LogLevel.HIGHLIGHT)) {\n console.log(this.format(LogLevel.HIGHLIGHT, this.colorize(message, colors.bright + colors.magenta)));\n }\n }\n\n /**\n * Check if message should be logged based on level\n */\n private shouldLog(level: LogLevel): boolean {\n const levels = [\n LogLevel.DEBUG,\n LogLevel.INFO,\n LogLevel.SUCCESS,\n LogLevel.WARNING,\n LogLevel.ERROR,\n LogLevel.HIGHLIGHT,\n ];\n return levels.indexOf(level) >= levels.indexOf(this.config.level);\n }\n\n /**\n * Set log level\n */\n setLevel(level: LogLevel): void {\n this.config.level = level;\n }\n}\n\n// Default logger instance\nexport const logger: Logger = new Logger();\n","import type { ResolvedConfig } from '../config/config-schema.js';\n\nexport type DownloadOptions = {\n downloadDir: string;\n tempDir?: string;\n cookieFile?: string;\n minDuration: number;\n subLangs?: string[];\n};\n\nexport function extractDownloadOptions(resolvedConfig: ResolvedConfig<'series'>): DownloadOptions {\n return {\n downloadDir: resolvedConfig.download.downloadDir,\n tempDir: resolvedConfig.download.tempDir,\n cookieFile: resolvedConfig.cookieFile,\n minDuration: resolvedConfig.download.minDuration,\n subLangs: resolvedConfig.subLangs,\n };\n}\n","import type { Episode } from '../types/episode.types';\nimport type { Downloader, DownloaderOptions, DownloadResult } from './types';\n\nexport abstract class BaseDownloader implements Downloader {\n abstract getName(): string;\n abstract supports(url: string): boolean;\n abstract download(\n episode: Episode,\n dir: string,\n filenameWithoutExt: string,\n options?: DownloaderOptions,\n ): Promise<DownloadResult>;\n}\n","import * as fsPromises from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\n\nexport type YtdlpWrapperOptions = {\n /** Additional yt-dlp CLI arguments (flexible - any yt-dlp args) */\n args?: string[];\n /** Path to cookie file (Netscape format) */\n cookieFile?: string;\n /** List of subtitle languages to download (e.g., ['en', 'ru']) */\n subLangs?: string[];\n /** Callback for progress updates */\n onProgress?: (progress: string) => void;\n /** Callback for log messages */\n onLog?: (message: string) => void;\n};\n\nexport type YtdlpDownloadResult = {\n /** Main file path */\n filename: string;\n /** All downloaded/generated files */\n allFiles: string[];\n};\n\n/**\n * Low-level wrapper for yt-dlp CLI\n * Provides flexible interface for running yt-dlp with arbitrary arguments\n */\nexport class YtdlpWrapper {\n /**\n * Download using yt-dlp with custom arguments\n *\n * @param url - Video URL\n * @param outputName - Output filename without extension (for -o template)\n * @param dir - Target directory\n * @param options - Wrapper options\n * @returns Download result with filenames\n */\n async download(\n url: string,\n outputName: string,\n dir: string,\n options: YtdlpWrapperOptions = {},\n ): Promise<YtdlpDownloadResult> {\n const { args = [], cookieFile, subLangs, onProgress, onLog } = options;\n\n // Build output template\n const outputTemplate = join(dir, `${outputName}.%(ext)s`);\n\n // Ensure directory exists\n await fsPromises.mkdir(dir, { recursive: true });\n\n // Build command arguments\n const cmdArgs = ['--no-warnings', '--newline', '-o', outputTemplate];\n\n // Add cookie file if provided\n if (cookieFile) {\n cmdArgs.unshift('--cookies', cookieFile);\n }\n\n // Add subtitle arguments if subLangs is provided\n if (subLangs && subLangs.length > 0) {\n cmdArgs.push('--write-subs', '--sub-lang', subLangs.join(','));\n }\n\n // Add user-provided args (flexible - can override defaults if needed)\n cmdArgs.push(...args);\n\n // Add URL at the end (unless it's already in args)\n if (!args.some((arg) => arg === url)) {\n cmdArgs.push(url);\n }\n\n let filename: string | null = null;\n const allFiles: Set<string> = new Set();\n const outputBuffer: string[] = [];\n\n try {\n const subprocess = execa('yt-dlp', cmdArgs, { all: true });\n\n if (subprocess.all) {\n for await (const line of subprocess.all) {\n const text = line.toString().trim();\n if (!text) continue;\n\n outputBuffer.push(text);\n\n // Parse output (same logic as current YtDlpDownloader)\n const destMatch = text.match(/\\[download\\] Destination:\\s*(.+)/);\n if (destMatch) {\n filename = destMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n const subMatch = text.match(/\\[info\\] Writing video subtitles to:\\s*(.+)/);\n if (subMatch?.[1]) {\n allFiles.add(subMatch[1]);\n }\n\n const mergeMatch = text.match(/\\[merge\\] Merging formats into \"(.*)\"/);\n if (mergeMatch) {\n filename = mergeMatch[1];\n if (filename) allFiles.add(filename);\n }\n\n // Status messages\n if (\n text.startsWith('[info]') ||\n text.startsWith('[ffmpeg]') ||\n text.startsWith('[merge]') ||\n text.startsWith('[ExtractAudio]') ||\n text.startsWith('[Metadata]') ||\n text.startsWith('[Thumbnails]')\n ) {\n onLog?.(text);\n continue;\n }\n\n // Progress lines\n if (text.startsWith('[download]')) {\n const progressMatch = text.match(\n /\\[download\\]\\s+(\\d+\\.?\\d*)%\\s+of\\s+~?\\s*([\\d.]+\\w+)\\s+at\\s+~?\\s*([\\d.]+\\w+\\/s)\\s+ETA\\s+(\\S+)/,\n );\n\n if (progressMatch) {\n const [, percentage, totalSize, speed, eta] = progressMatch;\n onProgress?.(`[download] ${percentage}% of ${totalSize} at ${speed} ETA ${eta}`);\n } else {\n onLog?.(text);\n }\n continue;\n }\n\n onLog?.(text);\n }\n }\n\n await subprocess;\n\n if (!filename) {\n throw new Error('Could not determine downloaded filename from output');\n }\n\n return {\n filename,\n allFiles: Array.from(allFiles),\n };\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n const fullLog = outputBuffer.join('\\n');\n throw new Error(`yt-dlp failed: ${errorMsg}\\n\\nLog output:\\n${fullLog}`);\n }\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkInstalled(): Promise<boolean> {\n try {\n await execa('yt-dlp', ['--version']);\n return true;\n } catch {\n return false;\n }\n }\n}\n","import type { Episode } from '../../types/episode.types.js';\nimport { BaseDownloader } from '../base-downloader.js';\nimport { YtdlpWrapper } from '../lib/ytdlp-wrapper.js';\nimport type { DownloaderOptions, DownloadResult } from '../types.js';\n\nexport class YtDlpDownloader extends BaseDownloader {\n private wrapper = new YtdlpWrapper();\n\n getName(): string {\n return 'yt-dlp';\n }\n\n supports(_url: string): boolean {\n return true; // Default downloader supports everything (or tries to)\n }\n\n async download(\n episode: Episode,\n dir: string,\n filenameWithoutExt: string,\n options?: DownloaderOptions,\n ): Promise<DownloadResult> {\n // Use wrapper with preset args for video download\n // Note: wrapper already handles URL, so we don't need to pass it in args\n return this.wrapper.download(episode.url, filenameWithoutExt, dir, {\n args: [], // No additional args needed for basic download\n cookieFile: options?.cookieFile,\n subLangs: options?.subLangs,\n onProgress: options?.onProgress,\n onLog: options?.onLog,\n });\n }\n\n /**\n * Check if yt-dlp is installed\n */\n static async checkInstalled(): Promise<boolean> {\n return YtdlpWrapper.checkInstalled();\n }\n}\n","import { YtDlpDownloader } from './impl/yt-dlp-downloader';\nimport type { Downloader } from './types';\n\nexport class DownloaderRegistry {\n private downloaders: Downloader[] = [];\n private defaultDownloader: Downloader;\n\n constructor() {\n this.defaultDownloader = new YtDlpDownloader();\n }\n\n register(downloader: Downloader): void {\n this.downloaders.push(downloader);\n }\n\n getDownloader(url: string): Downloader {\n // Find first specific downloader that supports the URL\n // Since default downloader returns true for everything, we check it last\n // But here we iterate registered custom downloaders first\n for (const downloader of this.downloaders) {\n if (downloader.supports(url)) {\n return downloader;\n }\n }\n\n return this.defaultDownloader;\n }\n}\n\nexport const downloaderRegistry = new DownloaderRegistry();\n","import { HandlerError } from '../errors/custom-errors';\nimport type { DomainHandler, HandlerRegistry } from '../types/handler.types';\nimport { extractDomain } from '../utils/url-utils';\n\n/**\n * Handler registry implementation\n */\nexport class Registry implements HandlerRegistry {\n private handlers: Map<string, DomainHandler> = new Map();\n\n /**\n * Register a handler\n */\n register(handler: DomainHandler): void {\n this.handlers.set(handler.getDomain(), handler);\n }\n\n /**\n * Get handler for URL\n */\n getHandler(url: string): DomainHandler | undefined {\n const domain = extractDomain(url);\n\n // First try exact match\n if (this.handlers.has(domain)) {\n return this.handlers.get(domain);\n }\n\n // Then try subdomain match (e.g., www.wetv.vip -> wetv.vip)\n for (const [handlerDomain, handler] of this.handlers.entries()) {\n if (domain === handlerDomain || domain.endsWith(`.${handlerDomain}`) || handlerDomain.endsWith(`.${domain}`)) {\n return handler;\n }\n }\n\n return undefined;\n }\n\n /**\n * Get all registered domains\n */\n getDomains(): string[] {\n return Array.from(this.handlers.keys());\n }\n\n /**\n * Get handler or throw error\n */\n getHandlerOrThrow(url: string): DomainHandler {\n const handler = this.getHandler(url);\n if (!handler) {\n throw new HandlerError(\n `No handler found for domain: \"${extractDomain(url)}\". ` + `Supported domains: ${this.getDomains().join(', ')}`,\n url,\n );\n }\n return handler;\n }\n}\n\n// Global registry instance\nexport const handlerRegistry: Registry = new Registry();\n","import * as cheerio from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport { HandlerError } from '../../errors/custom-errors';\nimport type { Episode, EpisodeType } from '../../types/episode.types';\nimport type { DomainHandler } from '../../types/handler.types';\nimport { extractDomain } from '../../utils/url-utils';\n\n/**\n * Base handler class with common functionality\n */\nexport abstract class BaseHandler implements DomainHandler {\n abstract getDomain(): string;\n\n abstract extractEpisodes(url: string, cookies?: string): Promise<Episode[]>;\n\n /**\n * Check if handler supports the given URL\n */\n supports(url: string): boolean {\n try {\n const domain = extractDomain(url);\n return domain === this.getDomain() || domain.endsWith(`.${this.getDomain()}`);\n } catch {\n return false;\n }\n }\n\n /**\n * Fetch HTML from URL with optional cookies\n */\n protected async fetchHtml(url: string, cookies?: string): Promise<string> {\n const headers: Record<string, string> = {\n 'User-Agent':\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n 'Accept-Language': 'en-US,en;q=0.9',\n };\n\n if (cookies) {\n headers.Cookie = cookies;\n }\n\n try {\n const response = await fetch(url, { headers });\n\n if (!response.ok) {\n throw new HandlerError(`HTTP ${response.status}: ${response.statusText}`, url);\n }\n\n return await response.text();\n } catch (error) {\n if (error instanceof HandlerError) {\n throw error;\n }\n throw new HandlerError(`Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`, url);\n }\n }\n\n /**\n * Parse cheerio document from HTML\n */\n protected parseHtml(html: string): cheerio.CheerioAPI {\n return cheerio.load(html);\n }\n\n /**\n * Parse episode number from text\n * Handles formats like \"第1集\", \"EP1\", \"Episode 1\", etc.\n */\n protected parseEpisodeNumber(text: string): number | null {\n // Chinese format: 第X集\n const chineseMatch = text.match(/第(\\d+)集/);\n if (chineseMatch?.[1]) {\n return parseInt(chineseMatch[1], 10);\n }\n\n // EP prefix: EP1, ep01, etc.\n const epMatch = text.match(/ep\\s?(\\d+)/i);\n if (epMatch?.[1]) {\n return parseInt(epMatch[1], 10);\n }\n\n // Episode prefix: Episode 1, E1, etc.\n const episodeMatch = text.match(/(?:episode|e)\\s?(\\d+)/i);\n if (episodeMatch?.[1]) {\n return parseInt(episodeMatch[1], 10);\n }\n\n // Standalone number\n const numberMatch = text.match(/\\b(\\d+)\\b/);\n if (numberMatch?.[1]) {\n return parseInt(numberMatch[1], 10);\n }\n\n return null;\n }\n\n /**\n * Parse episode type from class names or text\n */\n protected parseEpisodeType(element: AnyNode, $: cheerio.CheerioAPI): EpisodeType {\n const $el = $(element);\n const className = $el.attr('class') || '';\n const text = $el.text().toLowerCase();\n\n // Check for VIP indicators\n if (className.includes('vip') || text.includes('vip') || text.includes('会员')) {\n return 'vip' as EpisodeType;\n }\n\n // Check for preview indicators\n if (\n className.includes('preview') ||\n className.includes('trailer') ||\n text.includes('preview') ||\n text.includes('预告')\n ) {\n return 'preview' as EpisodeType;\n }\n\n // Check for locked indicators\n if (\n className.includes('locked') ||\n className.includes('lock') ||\n text.includes('locked') ||\n text.includes('锁定')\n ) {\n return 'locked' as EpisodeType;\n }\n\n return 'available' as EpisodeType;\n }\n}\n","import type { CheerioAPI } from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype NextData = {\n props?: {\n pageProps?: {\n data?: string;\n };\n };\n};\n\ntype PageData = {\n albumInfo?: {\n albumId: string;\n title: string;\n };\n videoList?: Array<{\n vid: string;\n episode: string;\n title: string;\n isTrailer?: number | boolean;\n payStatus?: number;\n }>;\n};\n\n/**\n * Handler for iq.com domain (iQIYI international)\n */\nexport class IQiyiHandler extends BaseHandler {\n getDomain(): string {\n return 'iq.com';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n\n // Try to extract from __NEXT_DATA__ first (includes all episodes)\n const nextDataEpisodes = this.extractFromNextData(html);\n if (nextDataEpisodes.length > 0) {\n return nextDataEpisodes;\n }\n\n // Fallback to HTML parsing (old method)\n return this.extractFromHtml(html);\n }\n\n /**\n * Extract episodes from __NEXT_DATA__ JSON embedded in HTML\n * This method gets ALL episodes including those not visible due to pagination\n */\n private extractFromNextData(html: string): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n // Extract __NEXT_DATA__ script content\n const match = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>(.+?)<\\/script>/s);\n if (!match || !match[1]) {\n return episodes;\n }\n\n const nextData: NextData = JSON.parse(match[1]);\n const dataStr = nextData.props?.pageProps?.data;\n if (!dataStr) return episodes;\n\n const pageData: PageData = JSON.parse(dataStr as string);\n\n const { albumInfo, videoList = [] } = pageData;\n const { albumId, title } = albumInfo || {};\n\n // Process each video\n for (const video of videoList) {\n const { vid, episode, isTrailer, payStatus } = video;\n\n // Skip trailers\n if (isTrailer) {\n continue;\n }\n\n const episodeNumber = this.parseEpisodeNumber(episode);\n if (!episodeNumber) {\n continue;\n }\n\n // Build URL: /play/{albumId}-{vid}?lang=en_us\n const episodeUrl = `https://www.iq.com/play/${albumId}-${vid}?lang=en_us`;\n\n // Determine episode type based on payStatus\n const type = this.determineTypeFromPayStatus(payStatus);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: `${title} - Episode ${episode}`,\n extractedAt: new Date(),\n });\n }\n } catch (error) {\n // If extraction fails, return empty array to trigger fallback\n console.error('Failed to extract from __NEXT_DATA__:', error);\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Extract episodes from HTML (fallback method)\n */\n private extractFromHtml(html: string): Episode[] {\n const $ = this.parseHtml(html);\n const episodes: Episode[] = [];\n\n // Try multiple selectors for episode lists\n const selectors = [\n 'ul li a[href*=\"/play/\"]', // Most common pattern\n '.album-episode-item a[href*=\"/play/\"]',\n '.episode-item a[href*=\"/play/\"]',\n '.intl-play-item a[href*=\"/play/\"]',\n '[data-episode] a[href*=\"/play/\"]',\n ];\n\n for (const selector of selectors) {\n const links = $(selector);\n\n if (links.length > 0) {\n links.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n\n // If we found episodes with this selector, break\n if (episodes.length > 0) {\n break;\n }\n }\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Determine episode type from payStatus\n */\n private determineTypeFromPayStatus(payStatus?: number): EpisodeType {\n if (payStatus === 6) {\n return EpisodeType.VIP;\n }\n return EpisodeType.AVAILABLE;\n }\n\n /**\n * Process a single episode link element\n */\n private processEpisodeLink($: CheerioAPI, element: AnyNode, episodes: Episode[]): void {\n const $el = $(element);\n const href = $el.attr('href');\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://www.iq.com${href}`;\n\n // Get text content for parsing\n const text = $el.text().trim();\n const title = $el.attr('title') || undefined;\n\n // Filter out BTS episodes (behind-the-scenes) - check text content FIRST\n // This must happen before extracting episode number from URL\n if (text.toUpperCase().includes('BTS')) {\n return;\n }\n\n // Extract episode number from text FIRST (more reliable than URL)\n let episodeNumber = this.parseEpisodeNumber(text);\n\n // If not found in text, try href (fallback)\n if (!episodeNumber) {\n episodeNumber = this.parseEpisodeNumber(href);\n }\n\n if (!episodeNumber) return;\n\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (exists) return;\n\n // Determine episode type based on VIP badges\n const type = this.determineEpisodeType($, element);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title,\n extractedAt: new Date(),\n });\n }\n\n /**\n * Determine episode type based on badges (VIP, etc.)\n */\n private determineEpisodeType($: CheerioAPI, element: AnyNode): EpisodeType {\n // Check parent elements for VIP badge\n const $parent = $(element).closest('li, div');\n\n if ($parent.length) {\n const parentText = $parent.text() || '';\n\n // Check for VIP badge\n if (parentText.toUpperCase().includes('VIP')) {\n return EpisodeType.VIP;\n }\n }\n\n // Default: available (free episodes)\n return EpisodeType.AVAILABLE;\n }\n}\n","import type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype MGTVResponse = {\n code: number;\n msg: string;\n data: {\n total_page: number;\n current_page: number;\n list: Array<{\n t1: string; // Episode number (e.g. \"1\")\n t2: string; // Title (e.g. \"EP 1\")\n t4: string; // Chinese title (e.g. \"第1集\")\n isvip: string; // \"1\" = VIP, \"0\" = Free\n url: string; // Relative URL (e.g. \"/b/823701/23967831.html\")\n video_id: string;\n clip_id: string;\n time: string; // Duration (e.g. \"45:00\")\n ts: string; // Timestamp\n }>;\n info: {\n title: string;\n isvip: string;\n };\n };\n};\n\n/**\n * Handler for mgtv.com domain\n */\nexport class MGTVHandler extends BaseHandler {\n getDomain(): string {\n return 'mgtv.com';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const videoId = this.extractVideoId(url);\n if (!videoId) {\n throw new Error('Could not extract video ID from URL');\n }\n\n const episodes: Episode[] = [];\n let page = 0;\n let totalPages = 1;\n\n do {\n const apiUrl = `https://tinker.glb.mgtv.com/episode/list?src=intelmgtv&abroad=10&_support=10000000&version=5.5.35&video_id=${videoId}&page=${page}&size=50&platform=4`;\n\n const response = await this.fetchHtml(apiUrl, cookies);\n let data: MGTVResponse;\n\n try {\n data = JSON.parse(response);\n } catch (_e) {\n throw new Error('Failed to parse MGTV API response');\n }\n\n if (data.code !== 200) {\n throw new Error(`MGTV API error: ${data.msg}`);\n }\n\n totalPages = data.data.total_page;\n\n for (const item of data.data.list) {\n const episodeNumber = this.parseEpisodeNumber(item.t1);\n if (!episodeNumber) continue;\n\n const episodeUrl = `https://w.mgtv.com${item.url}`;\n\n episodes.push({\n number: episodeNumber,\n title: item.t2 || item.t4 || `Episode ${episodeNumber}`,\n url: episodeUrl,\n type: item.isvip === '1' ? EpisodeType.VIP : EpisodeType.AVAILABLE,\n extractedAt: new Date(),\n });\n }\n\n page++;\n } while (page < totalPages);\n\n // Deduplicate and sort\n return this.deduplicateEpisodes(episodes);\n }\n\n private extractVideoId(url: string): string | null {\n // Matches /b/823701/23967831.html -> 23967831\n const match = url.match(/\\/b\\/\\d+\\/(\\d+)\\.html/);\n return match ? match[1] || null : null;\n }\n\n /**\n * Deduplicate episodes based on episode number\n */\n private deduplicateEpisodes(episodes: Episode[]): Episode[] {\n const uniqueEpisodes = new Map<number, Episode>();\n\n for (const episode of episodes) {\n if (!uniqueEpisodes.has(episode.number)) {\n uniqueEpisodes.set(episode.number, episode);\n }\n }\n\n return Array.from(uniqueEpisodes.values()).sort((a, b) => a.number - b.number);\n }\n}\n","import type { CheerioAPI } from 'cheerio';\nimport type { AnyNode } from 'domhandler';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { BaseHandler } from '../base/base-handler';\n\ntype NextData = {\n props?: {\n pageProps?: {\n data?: string;\n };\n };\n};\n\ntype PageData = {\n canPlay: boolean;\n coverInfo: {\n cid: string;\n title: string;\n secondTitle?: string;\n };\n videoList: Array<{\n vid: string;\n episode: string;\n isTrailer: number | boolean;\n payStatus?: number;\n defaultPayStatus?: number;\n coverList?: string[];\n labels?: {\n [key: string]: {\n text: string;\n color?: string;\n };\n };\n }>;\n};\n\n/**\n * Handler for wetv.vip domain\n */\nexport class WeTVHandler extends BaseHandler {\n getDomain(): string {\n return 'wetv.vip';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const html = await this.fetchHtml(url, cookies);\n\n // Try to extract from __NEXT_DATA__ first (includes all episodes)\n const nextDataEpisodes = this.extractFromNextData(html);\n if (nextDataEpisodes.length > 0) {\n return nextDataEpisodes;\n }\n\n // Fallback to HTML parsing (old method)\n return this.extractFromHtml(html);\n }\n\n /**\n * Extract episodes from __NEXT_DATA__ JSON embedded in HTML\n * This method gets ALL episodes including those not visible due to pagination\n */\n private extractFromNextData(html: string): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n // Extract __NEXT_DATA__ script content\n const match = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>(.+?)<\\/script>/s);\n if (!match || !match[1]) {\n return episodes;\n }\n\n const nextData: NextData = JSON.parse(match[1]);\n const dataStr = nextData.props?.pageProps?.data;\n if (!dataStr) return episodes;\n const pageData: PageData = JSON.parse(dataStr as string);\n\n const { coverInfo, videoList = [] } = pageData;\n const { cid, title } = coverInfo;\n\n // Extract CID from first video's coverList if not available in coverInfo\n const coverId = videoList[0]?.coverList?.[0] || cid;\n\n // Process each video\n for (const video of videoList) {\n const { vid, episode, isTrailer } = video;\n\n // Skip trailers/teasers\n if (isTrailer) {\n continue;\n }\n\n const episodeNumber = this.parseEpisodeNumber(episode);\n if (!episodeNumber) {\n continue;\n }\n\n // Build URL: /en/play/{cid}/{vid}-EP{episode}%3A{title}\n const encodedTitle = encodeURIComponent(title);\n const episodeUrl = `https://wetv.vip/en/play/${coverId}/${vid}-EP${episode}%3A${encodedTitle}`;\n\n // Determine episode type based on labels and payStatus\n const type = this.determineTypeFromVideo(video);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: `${title} - Episode ${episode}`,\n extractedAt: new Date(),\n });\n }\n } catch (error) {\n // If extraction fails, return empty array to trigger fallback\n console.error('Failed to extract from __NEXT_DATA__:', error);\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Extract episodes from HTML (fallback method)\n * This only gets visible episodes (first 30 due to pagination)\n */\n private extractFromHtml(html: string): Episode[] {\n const $ = this.parseHtml(html);\n const episodes: Episode[] = [];\n\n // WeTV uses play-video__link class for episode links\n const episodeLinks = $('a.play-video__link[href*=\"/play/\"][href*=\"EP\"]');\n\n if (episodeLinks.length === 0) {\n // Fallback: try generic selector\n const fallbackLinks = $('a[href*=\"/play/\"]').filter((_, el) => {\n const href = $(el).attr('href') || '';\n return href.includes('EP');\n });\n\n fallbackLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n } else {\n episodeLinks.each((_, element) => {\n this.processEpisodeLink($, element, episodes);\n });\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n\n /**\n * Determine episode type from video data\n * Checks labels first, then falls back to payStatus\n */\n private determineTypeFromVideo(video: PageData['videoList'][0]): EpisodeType {\n const { labels, payStatus, defaultPayStatus } = video;\n\n // Check labels first (more reliable)\n if (labels) {\n for (const key in labels) {\n const label = labels[key];\n if (!label) continue;\n const labelText = label.text?.toLowerCase() || '';\n\n if (labelText === 'express') {\n return EpisodeType.EXPRESS;\n }\n if (labelText === 'teaser') {\n return EpisodeType.TEASER;\n }\n if (labelText === 'vip') {\n return EpisodeType.VIP;\n }\n }\n }\n\n // Fallback to payStatus\n const status = payStatus || defaultPayStatus;\n if (status === 6) {\n return EpisodeType.VIP;\n }\n if (status === 12) {\n return EpisodeType.EXPRESS;\n }\n return EpisodeType.AVAILABLE;\n }\n\n /**\n * Process a single episode link element\n */\n private processEpisodeLink($: CheerioAPI, element: AnyNode, episodes: Episode[]): void {\n const $el = $(element);\n const href = $el.attr('href');\n\n if (!href) return;\n\n // Build full URL if relative\n const episodeUrl = href.startsWith('http') ? href : `https://wetv.vip${href}`;\n\n // Extract episode number from aria-label (e.g., \"Play episode 01\")\n const ariaLabel = $el.attr('aria-label') || '';\n const episodeNumber = this.parseEpisodeNumber(ariaLabel);\n\n if (!episodeNumber) return;\n\n // Check if already added\n const exists = episodes.some((ep) => ep.number === episodeNumber);\n if (exists) return;\n\n // Determine episode type based on badges\n const type = this.determineEpisodeType($, element);\n\n episodes.push({\n number: episodeNumber,\n url: episodeUrl,\n type,\n title: $el.attr('title') || undefined,\n extractedAt: new Date(),\n });\n }\n\n /**\n * Determine episode type based on badges (VIP, Teaser, Express)\n */\n private determineEpisodeType($: CheerioAPI, element: AnyNode): EpisodeType {\n // Check for badges in parent li or sibling elements\n const $li = $(element).closest('li');\n\n if ($li.length) {\n // Look for span.play-video__label\n const badge = $li.find('span.play-video__label').first();\n\n if (badge.length) {\n const badgeText = badge.text().trim().toLowerCase();\n\n // Check badge types\n if (badgeText === 'vip' || badgeText.includes('vip')) {\n return EpisodeType.VIP;\n }\n if (badgeText === 'teaser' || badgeText.includes('teaser')) {\n return EpisodeType.TEASER;\n }\n if (badgeText === 'express' || badgeText.includes('express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Also check text content for badges\n const liText = $li.text() || '';\n if (liText.includes('VIP') && !liText.includes('Teaser')) {\n return EpisodeType.VIP;\n }\n if (liText.includes('Teaser')) {\n return EpisodeType.TEASER;\n }\n if (liText.includes('Express')) {\n return EpisodeType.EXPRESS;\n }\n }\n\n // Default: available (free episodes)\n return EpisodeType.AVAILABLE;\n }\n}\n","import { chromium } from 'playwright';\nimport type { Episode } from '../../types/episode.types';\nimport { EpisodeType } from '../../types/episode.types';\nimport { logger } from '../../utils/logger';\nimport { BaseHandler } from '../base/base-handler';\n\ntype EpisodeNode = {\n data?: {\n stage: number;\n title: string;\n paid: number;\n action?: {\n value: string;\n };\n };\n};\n\ntype EpisodeComponentNode = {\n data?: {\n pageIndex?: number;\n pageSize?: number;\n lastStage?: number;\n };\n nodes?: EpisodeNode[];\n typeName?: string;\n};\n\ntype InitialData = {\n data?: {\n data?: {\n nodes?: Array<{\n nodes?: EpisodeComponentNode[];\n }>;\n };\n };\n};\n\ntype MtopApiResponse = {\n data?: {\n [key: string]: {\n data?: {\n nodes?: EpisodeNode[];\n };\n };\n };\n};\n\n// Resource types to block for faster loading\nconst BLOCKED_RESOURCE_TYPES = ['font', 'stylesheet', 'image', 'media', 'manifest', 'websocket'];\n\n/**\n * Handler for youku.tv domain\n *\n * Uses Playwright to load the page with JavaScript execution enabled.\n * Intercepts the mtop API call that loads additional episodes.\n * Unnecessary resources (fonts, CSS, images, media) are blocked for faster loading.\n */\nexport class YoukuHandler extends BaseHandler {\n getDomain(): string {\n return 'youku.tv';\n }\n\n async extractEpisodes(url: string, cookies?: string): Promise<Episode[]> {\n const videoId = this.extractVideoId(url);\n if (!videoId) {\n throw new Error('Could not extract video ID from URL');\n }\n\n // Use Playwright to load page and extract episodes\n const episodes = await this.extractWithPlaywright(url, cookies);\n\n return episodes;\n }\n\n /**\n * Extract video ID from URL\n * Matches: https://www.youku.tv/v/v_show/id_XNjQ3MzIwNzE1Mg==.html\n */\n private extractVideoId(url: string): string | null {\n const match = url.match(/id_([^=]+)==\\.html/);\n return match?.[1] ? match[1] : null;\n }\n\n /**\n * Load page with Playwright and extract all episodes\n *\n * Blocks unnecessary resources for faster loading:\n * - Fonts\n * - CSS stylesheets\n * - Images\n * - Media (video/audio)\n * - Manifests\n * - WebSockets\n */\n private async extractWithPlaywright(url: string, cookies?: string): Promise<Episode[]> {\n const browser = await chromium.launch({\n headless: true,\n });\n\n try {\n const context = await browser.newContext({\n userAgent:\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n\n // Set cookies if provided\n if (cookies) {\n await context.addCookies([\n {\n name: '_m_h5_tk',\n value: cookies,\n domain: '.youku.tv',\n path: '/',\n },\n ]);\n }\n\n const page = await context.newPage();\n\n // Track API responses for additional episodes\n const apiResponses: MtopApiResponse[] = [];\n\n // Intercept API responses to capture episode data\n page.on('response', async (response) => {\n const requestUrl = response.url();\n\n // Look for mtop API responses with itemStartStage (pagination)\n if (requestUrl.includes('mtop.youku.columbus') && requestUrl.includes('itemStartStage')) {\n try {\n const contentType = response.headers()['content-type'] || '';\n if (contentType.includes('application/json')) {\n const body = await response.text();\n logger.debug(`Intercepted mtop API response (${body.length} chars)`);\n apiResponses.push(JSON.parse(body));\n }\n } catch (e) {\n logger.debug(`Error parsing API response: ${e}`);\n }\n }\n });\n\n // Block unnecessary resources for faster loading\n await page.route('**/*', (route) => {\n const resourceType = route.request().resourceType();\n if (BLOCKED_RESOURCE_TYPES.includes(resourceType)) {\n route.abort();\n } else {\n route.continue();\n }\n });\n\n // Navigate to the page and wait for episode component to load\n await page.goto(url, {\n waitUntil: 'domcontentloaded',\n timeout: 30000,\n });\n\n // Wait for initial episode data to be available\n await page.waitForFunction(\n () => {\n // @ts-expect-error - __INITIAL_DATA__ is defined on window\n const data = window.__INITIAL_DATA__;\n if (!data?.data?.data?.nodes) return false;\n\n const nodes = data.data.data.nodes;\n if (!nodes[0]?.nodes?.[1]?.data) return false;\n\n const episodeComponent = nodes[0].nodes[1];\n const episodes = episodeComponent.nodes || [];\n return episodes.length > 0;\n },\n { timeout: 15000 },\n );\n\n // Check initial episode count\n const initialInfo = await page.evaluate(() => {\n // @ts-expect-error - __INITIAL_DATA__ is defined on window\n const data = window.__INITIAL_DATA__;\n const nodes = data?.data?.data?.nodes;\n const episodeComponent = nodes?.[0]?.nodes?.[1];\n const episodeNodes = episodeComponent?.nodes || [];\n const { lastStage = 0 } = episodeComponent?.data || {};\n\n return {\n loaded: episodeNodes.length,\n total: lastStage,\n };\n });\n\n logger.info(`Episodes: ${initialInfo.loaded} loaded, ${initialInfo.total} total`);\n\n // If there are more episodes, wait for the API calls\n if (initialInfo.total > initialInfo.loaded) {\n logger.info(`Waiting for API to load remaining ${initialInfo.total - initialInfo.loaded} episodes...`);\n\n // Wait for all API responses or timeout\n const startTime = Date.now();\n const maxWait = 15000; // Increased timeout for multiple API calls\n\n // Keep waiting as long as we haven't reached all episodes and haven't timed out\n while (Date.now() - startTime < maxWait) {\n await page.waitForTimeout(500);\n\n // Calculate how many episodes we have so far\n const currentInfo = await page.evaluate(() => {\n // @ts-expect-error - __INITIAL_DATA__ is defined on window\n const data = window.__INITIAL_DATA__;\n const nodes = data?.data?.data?.nodes;\n const episodeComponent = nodes?.[0]?.nodes?.[1];\n const episodeNodes = episodeComponent?.nodes || [];\n const { lastStage = 0 } = episodeComponent?.data || {};\n return { loaded: episodeNodes.length, total: lastStage };\n });\n\n // Stop waiting if we've captured several API responses or have all episodes\n if (currentInfo.loaded >= currentInfo.total || apiResponses.length >= 3) {\n logger.success(\n `Captured ${apiResponses.length} API response(s), loaded ${currentInfo.loaded}/${currentInfo.total} episodes`,\n );\n break;\n }\n }\n\n if (apiResponses.length === 0) {\n logger.warning(`No API responses captured within timeout`);\n }\n }\n\n // Extract __INITIAL_DATA__ from the page\n const initialData = await page.evaluate(() => {\n // @ts-expect-error - __INITIAL_DATA__ is defined on window\n return window.__INITIAL_DATA__;\n });\n\n if (!initialData) {\n throw new Error('Could not find __INITIAL_DATA__ in page');\n }\n\n await context.close();\n\n // Parse episodes from both sources\n const episodes = this.parseInitialData(initialData);\n\n // If we have additional episodes from API, merge them all\n if (apiResponses.length > 0) {\n logger.info(`Processing ${apiResponses.length} API response(s)...`);\n\n for (const apiResponse of apiResponses) {\n const additionalEpisodes = this.parseApiResponse(apiResponse);\n logger.info(` Merging ${additionalEpisodes.length} additional episodes`);\n\n // Merge episode lists, avoiding duplicates\n const existingIds = new Set(episodes.map((ep) => ep.number));\n for (const ep of additionalEpisodes) {\n if (!existingIds.has(ep.number)) {\n episodes.push(ep);\n existingIds.add(ep.number);\n }\n }\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n }\n\n logger.success(`Total episodes extracted: ${episodes.length}`);\n return episodes;\n } finally {\n await browser.close();\n }\n }\n\n /**\n * Parse episodes from mtop API response\n */\n private parseApiResponse(apiResponse: MtopApiResponse): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n if (!apiResponse.data) {\n return episodes;\n }\n\n // Navigate to episode nodes: data[\"2019030100\"].data.nodes\n const keys = Object.keys(apiResponse.data);\n for (const key of keys) {\n const value = apiResponse.data[key];\n\n if (value?.data?.nodes) {\n const episodeNodes = value.data.nodes || [];\n logger.debug(`API response: found ${episodeNodes.length} additional episodes`);\n\n for (const item of episodeNodes) {\n if (!item.data) continue;\n\n const { stage, title, paid } = item.data;\n const videoId = item.data?.action?.value;\n\n // Skip non-episode content\n if (!stage || stage < 1) {\n continue;\n }\n\n // Build URL for each episode\n const episodeUrl = `https://www.youku.tv/v/v_show/id_${videoId}.html`;\n\n // Determine episode type\n const type = paid === 1 ? EpisodeType.VIP : EpisodeType.AVAILABLE;\n\n episodes.push({\n number: stage,\n title: title || `Episode ${stage}`,\n url: episodeUrl,\n type,\n extractedAt: new Date(),\n });\n }\n\n break;\n }\n }\n } catch (error) {\n logger.error(`Failed to parse API response: ${error}`);\n }\n\n return episodes;\n }\n\n /**\n * Parse episodes from __INITIAL_DATA__ object\n */\n private parseInitialData(initialData: InitialData): Episode[] {\n const episodes: Episode[] = [];\n\n try {\n // Navigate to episode list: data.data.nodes[0].nodes[1]\n const nodes = initialData.data?.data?.nodes;\n if (!nodes || nodes.length === 0) {\n throw new Error('No nodes found in INITIAL_DATA');\n }\n\n // Find episode component (nodes[1] is typically \"Web播放页选集组件\")\n const episodeComponent = nodes[0]?.nodes?.[1];\n if (!episodeComponent) {\n throw new Error('Episode component not found');\n }\n\n if (!episodeComponent.data) {\n throw new Error('Episode component has no data');\n }\n\n const { lastStage = 0 } = episodeComponent.data;\n const episodeNodes = episodeComponent.nodes || [];\n\n logger.debug(`__INITIAL_DATA__: found ${episodeNodes.length} episodes (total: ${lastStage})`);\n\n for (const item of episodeNodes) {\n if (!item.data) continue;\n\n const { stage, title, paid } = item.data;\n const videoId = item.data?.action?.value;\n\n // Skip non-episode content\n if (!stage || stage < 1) {\n continue;\n }\n\n // Build URL for each episode\n const episodeUrl = `https://www.youku.tv/v/v_show/id_${videoId}.html`;\n\n // Determine episode type\n const type = paid === 1 ? EpisodeType.VIP : EpisodeType.AVAILABLE;\n\n episodes.push({\n number: stage,\n title: title || `Episode ${stage}`,\n url: episodeUrl,\n type,\n extractedAt: new Date(),\n });\n }\n } catch (error) {\n logger.error(`Failed to parse episodes from __INITIAL_DATA__: ${error}`);\n throw error;\n }\n\n // Sort by episode number\n episodes.sort((a, b) => a.number - b.number);\n\n return episodes;\n }\n}\n","import { logger } from '../utils/logger';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Console notifier for terminal output\n */\nexport class ConsoleNotifier implements Notifier {\n private lastProgressLength = 0;\n\n notify(level: NotificationLevel, message: string): void {\n // If there was an active progress line, clear it first so the log appears cleanly\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n this.lastProgressLength = 0;\n }\n\n switch (level) {\n case NotificationLevel.INFO:\n logger.info(message);\n break;\n case NotificationLevel.SUCCESS:\n logger.success(message);\n break;\n case NotificationLevel.WARNING:\n logger.warning(message);\n break;\n case NotificationLevel.ERROR:\n logger.error(message);\n break;\n case NotificationLevel.HIGHLIGHT:\n logger.highlight(message);\n break;\n }\n }\n\n progress(message: string): void {\n // Clear previous progress by overwriting with spaces\n if (this.lastProgressLength > 0) {\n process.stdout.write(`\\r${' '.repeat(this.lastProgressLength)}\\r`);\n }\n\n // Write new progress message\n process.stdout.write(`\\r${message}`);\n this.lastProgressLength = message.length;\n }\n\n /**\n * Finalize progress (add newline after last progress update)\n */\n endProgress(): void {\n if (this.lastProgressLength > 0) {\n process.stdout.write('\\n');\n this.lastProgressLength = 0;\n }\n }\n}\n","import { NotificationError } from '../errors/custom-errors';\nimport type { Notifier } from './notifier';\nimport { NotificationLevel } from './notifier';\n\n/**\n * Telegram configuration\n */\nexport type TelegramConfig = {\n botToken: string;\n chatId: string;\n};\n\n/**\n * Telegram notifier for error notifications only\n */\nexport class TelegramNotifier implements Notifier {\n private config: TelegramConfig;\n private apiUrl: string;\n\n constructor(config: TelegramConfig) {\n this.config = config;\n this.apiUrl = `https://api.telegram.org/bot${config.botToken}/sendMessage`;\n }\n\n /**\n * Send notification via Telegram\n * Only sends ERROR level notifications\n */\n async notify(level: NotificationLevel, message: string): Promise<void> {\n // Only send error notifications\n if (level !== NotificationLevel.ERROR) {\n return;\n }\n\n try {\n const emoji = this.getEmoji(level);\n // Truncate message if it's too long (Telegram limit is 4096 chars)\n // We reserve ~100 chars for header and tags\n const MAX_LENGTH = 4000;\n let safeMessage = message;\n if (safeMessage.length > MAX_LENGTH) {\n safeMessage = `${safeMessage.substring(0, MAX_LENGTH)}\\n... (truncated)`;\n }\n\n // Escape HTML characters in the message to prevent parsing errors\n const escapedMessage = this.escapeHtml(safeMessage);\n // Use <pre> tag for the error message to preserve formatting and monospacing\n const formattedMessage = `${emoji} <b>wetvlo Error</b>\\n\\n<pre>${escapedMessage}</pre>`;\n\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n chat_id: this.config.chatId,\n text: formattedMessage,\n parse_mode: 'HTML',\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new NotificationError(\n `Failed to send Telegram notification: ${response.status} ${response.statusText}\\n${errorText}`,\n );\n }\n } catch (error) {\n // Don't throw for notification errors, just log them\n console.error('Telegram notification failed:', error);\n }\n }\n\n /**\n * Escape HTML special characters\n */\n private escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n }\n\n /**\n * Get emoji for notification level\n */\n private getEmoji(level: NotificationLevel): string {\n switch (level) {\n case NotificationLevel.INFO:\n return 'ℹ️';\n case NotificationLevel.SUCCESS:\n return '✅';\n case NotificationLevel.WARNING:\n return '⚠️';\n case NotificationLevel.ERROR:\n return '❌';\n case NotificationLevel.HIGHLIGHT:\n return '🔔';\n default:\n return '';\n }\n }\n\n /**\n * Progress updates are not sent to Telegram (no-op)\n */\n async progress(_message: string): Promise<void> {\n // Telegram doesn't need real-time progress updates\n }\n\n /**\n * Progress finalization is no-op for Telegram\n */\n async endProgress(): Promise<void> {\n // No-op for Telegram\n }\n}\n","/**\n * QueueManager - Orchestrates check and download queues with UniversalScheduler\n *\n * Manages the queue-based architecture with:\n * - Per-domain check and download queues\n * - Universal scheduler for single-task execution globally\n * - Event-driven scheduling (no polling loops)\n * - Graceful shutdown\n * - Proper end-to-start cooldowns\n */\n\nimport { createHash } from 'node:crypto';\nimport { AppContext } from '../app-context.js';\nimport type { ResolvedConfig } from '../config/config-schema.js';\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { handlerRegistry } from '../handlers/handler-registry.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport type { StateManager } from '../state/state-manager.js';\nimport type { Episode, EpisodeType } from '../types/episode.types.js';\nimport { extractDomain } from '../utils/url-utils.js';\nimport type { CheckQueueItem, DownloadQueueItem } from './types.js';\nimport { UniversalScheduler } from './universal-scheduler.js';\n\n/**\n * Queue Manager for orchestrating all queues with universal scheduler\n */\nexport class QueueManager {\n private stateManager: StateManager;\n private downloadManager: DownloadManager;\n\n // Universal scheduler (handles all check and download queues)\n private scheduler: UniversalScheduler<CheckQueueItem | DownloadQueueItem>;\n\n // Running state\n private running = false;\n\n // Domain handlers cache\n private domainHandlers = new Map<string, ReturnType<typeof handlerRegistry.getHandlerOrThrow>>();\n\n /**\n * Create a new QueueManager\n *\n * @param downloadManager - Download manager instance\n * @param schedulerFactory - Optional factory for creating scheduler (for testing)\n */\n constructor(\n downloadManager: DownloadManager,\n schedulerFactory?: (\n executor: (task: CheckQueueItem | DownloadQueueItem, queueName: string) => Promise<void>,\n ) => UniversalScheduler<CheckQueueItem | DownloadQueueItem>,\n ) {\n // Get StateManager from AppContext\n this.stateManager = AppContext.getStateManager();\n this.downloadManager = downloadManager;\n\n // Create universal scheduler with executor callback\n const createScheduler = schedulerFactory || ((executor) => new UniversalScheduler(executor));\n this.scheduler = createScheduler(async (task, queueName) => {\n await this.executeTask(task, queueName);\n });\n\n // Set up wait notification\n this.scheduler.setOnWait((queueName, waitMs) => {\n const notifier = AppContext.getNotifier();\n const seconds = Math.round(waitMs / 1000);\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (type === 'download') {\n notifier.notify(NotificationLevel.INFO, `[${domain}] Next download in ${seconds}s...`);\n } else if (type === 'check') {\n notifier.notify(NotificationLevel.INFO, `[${domain}] Next check in ${seconds}s...`);\n }\n });\n }\n\n /**\n * Add a series to the check queue\n *\n * @param seriesUrl - Series URL\n */\n addSeriesCheck(seriesUrl: string): void {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const domain = extractDomain(seriesUrl);\n\n // Get series name from resolved config for notification\n const config = registry.resolve(seriesUrl, 'series');\n const seriesName = config.name;\n\n // Register download queue for this domain (shared across series)\n this.registerDownloadQueue(domain);\n\n // Register specific check queue for this series (isolated interval)\n const queueName = this.registerSeriesCheckQueue(domain, seriesUrl);\n\n // Add series to check queue\n const item: CheckQueueItem = {\n seriesUrl,\n attemptNumber: 1,\n retryCount: 0,\n };\n\n this.scheduler.addTask(queueName, item);\n\n notifier.notify(NotificationLevel.INFO, `[QueueManager] Added ${seriesName} to check queue for domain ${domain}`);\n }\n\n /**\n * Add episodes to the download queue\n *\n * @param seriesUrl - Series URL\n * @param episodes - Episodes to download\n */\n addEpisodes(seriesUrl: string, episodes: Episode[]): void {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n\n if (episodes.length === 0) {\n return;\n }\n\n // Get series name from resolved config for notification\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const domain = extractDomain(seriesUrl);\n\n // Register download queues for this domain if not already registered\n this.registerDownloadQueue(domain);\n\n // Get download delay from resolved config\n const { downloadDelay } = resolvedConfig.download;\n\n // Add episodes to download queue with staggered delays\n for (let i = 0; i < episodes.length; i++) {\n const episode = episodes[i];\n if (!episode) continue;\n\n const item: DownloadQueueItem = {\n seriesUrl,\n episode,\n retryCount: 0,\n };\n\n const queueName = `download:${domain}`;\n // Stagger episodes by downloadDelay\n const delayMs = i * downloadDelay * 1000;\n this.scheduler.addTask(queueName, item, delayMs);\n }\n\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[QueueManager] Added ${episodes.length} episodes to download queue for ${seriesName} (domain ${domain})`,\n );\n }\n\n /**\n * Update configuration\n */\n updateConfig(): void {\n const notifier = AppContext.getNotifier();\n // Config is reloaded in AppContext, we just need to notify\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Configuration will be reloaded from AppContext');\n }\n\n /**\n * Start all queues\n */\n start(): void {\n const notifier = AppContext.getNotifier();\n\n if (this.running) {\n throw new Error('QueueManager is already running');\n }\n\n this.running = true;\n this.scheduler.resume();\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Started queue processing');\n }\n\n /**\n * Stop all queues gracefully\n *\n * Waits for current task to complete.\n */\n async stop(): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n if (!this.running) {\n return;\n }\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Stopping queue processing...');\n\n this.scheduler.stop();\n this.running = false;\n\n notifier.notify(NotificationLevel.INFO, '[QueueManager] Queue processing stopped');\n }\n\n /**\n * Check if there is active processing or pending tasks\n *\n * @returns Whether scheduler is actively processing or has pending tasks\n */\n hasActiveProcessing(): boolean {\n return this.scheduler.isExecutorBusy() || this.scheduler.hasPendingTasks();\n }\n\n /**\n * Get queue statistics\n *\n * @returns Object with queue statistics\n */\n getQueueStats(): {\n checkQueues: Record<string, { length: number; processing: boolean }>;\n downloadQueues: Record<string, { length: number; processing: boolean }>;\n } {\n const stats = this.scheduler.getStats();\n const checkQueues: Record<string, { length: number; processing: boolean }> = {};\n const downloadQueues: Record<string, { length: number; processing: boolean }> = {};\n\n for (const [queueName, queueStats] of stats.entries()) {\n if (queueName.startsWith('check:')) {\n const parts = queueName.split(':');\n const domain = parts[1]; // Extract domain from check:domain:hash\n if (!domain) continue;\n\n if (!checkQueues[domain]) {\n checkQueues[domain] = { length: 0, processing: false };\n }\n\n checkQueues[domain].length += queueStats.queueLength;\n if (queueStats.isExecuting) {\n checkQueues[domain].processing = true;\n }\n } else if (queueName.startsWith('download:')) {\n const domain = queueName.slice(9); // Remove \"download:\" prefix\n downloadQueues[domain] = {\n length: queueStats.queueLength,\n processing: queueStats.isExecuting,\n };\n }\n }\n\n return { checkQueues, downloadQueues };\n }\n\n /**\n * Register download queue for a domain (shared across series)\n */\n private registerDownloadQueue(domain: string): void {\n const registry = AppContext.getConfig();\n const queueName = `download:${domain}`;\n\n // Check if queue is already registered\n if (this.scheduler.hasQueue(queueName)) {\n return;\n }\n\n // Resolve configuration - use any URL from this domain to get domain-level config\n const testUrl = `https://${domain}/`;\n const resolvedConfig = registry.resolve(testUrl, 'domain');\n const { downloadDelay } = resolvedConfig.download;\n\n // Register queue with scheduler\n this.scheduler.registerQueue(queueName, downloadDelay * 1000); // Convert to ms\n }\n\n /**\n * Register specific check queue for a series (isolated interval)\n */\n private registerSeriesCheckQueue(domain: string, seriesUrl: string): string {\n const registry = AppContext.getConfig();\n\n // Generate a short hash of the URL to ensure uniqueness and safe queue name\n const hash = createHash('md5').update(seriesUrl).digest('hex').substring(0, 12);\n const queueName = `check:${domain}:${hash}`;\n\n // Check if queue is already registered\n if (this.scheduler.hasQueue(queueName)) {\n return queueName;\n }\n\n // Resolve configuration\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const { checkInterval } = resolvedConfig.check;\n\n // Register queue with scheduler\n this.scheduler.registerQueue(queueName, checkInterval * 1000); // Convert to ms\n\n // Ensure we have a handler for this domain\n if (!this.domainHandlers.has(domain)) {\n const handler = handlerRegistry.getHandlerOrThrow(`https://${domain}/`);\n this.domainHandlers.set(domain, handler);\n }\n\n return queueName;\n }\n\n /**\n * Execute a task from the scheduler\n *\n * This is the executor callback that handles both check and download tasks.\n *\n * @param task - Task to execute\n * @param queueName - Queue name (format: \"check:domain\" or \"download:domain\")\n */\n private async executeTask(task: CheckQueueItem | DownloadQueueItem, queueName: string): Promise<void> {\n const parts = queueName.split(':');\n const type = parts[0];\n const domain = parts[1];\n\n if (!type || !domain) {\n throw new Error(`Invalid queue name format: ${queueName}`);\n }\n\n if (type === 'check') {\n await this.executeCheck(task as CheckQueueItem, domain, queueName);\n } else if (type === 'download') {\n await this.executeDownload(task as DownloadQueueItem, domain, queueName);\n } else {\n throw new Error(`Unknown queue type: ${type}`);\n }\n }\n\n /**\n * Execute a check task\n *\n * @param item - Check queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeCheck(item: CheckQueueItem, domain: string, queueName: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const { seriesUrl, attemptNumber, retryCount = 0 } = item;\n\n // Get handler for this domain\n const handler = this.domainHandlers.get(domain);\n if (!handler) {\n throw new Error(`No handler found for domain ${domain}`);\n }\n\n // Get settings\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const { count: checksCount, checkInterval } = resolvedConfig.check;\n\n try {\n // Perform the check\n const result = await this.performCheck(handler, seriesUrl, resolvedConfig, attemptNumber, domain);\n\n if (result.hasNewEpisodes) {\n // Episodes found - send to download queue, do NOT requeue\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Found ${result.episodes.length} new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Add episodes to download queue\n this.addEpisodes(seriesUrl, result.episodes);\n\n // Session complete - do not requeue\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // No episodes found - check if we should requeue\n if (attemptNumber < checksCount) {\n // Requeue with interval delay\n const intervalMs = checkInterval * 1000;\n const requeueDelay = result.requeueDelay ?? intervalMs;\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] No new episodes for ${seriesName} (attempt ${attemptNumber}/${checksCount}), requeueing in ${Math.round(requeueDelay / 1000)}s`,\n );\n\n // Requeue with incremented attempt number\n const requeuedItem: CheckQueueItem = {\n seriesUrl,\n attemptNumber: attemptNumber + 1,\n retryCount: 0,\n };\n\n this.scheduler.addTask(queueName, requeuedItem, requeueDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Checks exhausted - do not requeue\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checks exhausted for ${seriesName} (${checksCount} attempts with no new episodes)`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Get download settings for retry config\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with exponential backoff (convert seconds to ms)\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000,\n backoffMultiplier,\n jitterPercentage,\n );\n\n notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Check failed for ${seriesName}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count (same attempt number)\n const requeuedItem: CheckQueueItem = {\n seriesUrl,\n attemptNumber,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n } else {\n // Max retries exceeded - log error and give up\n notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to check ${seriesName} after ${retryCount} retry attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, checkInterval * 1000);\n }\n }\n }\n\n /**\n * Execute a download task\n *\n * @param item - Download queue item\n * @param domain - Domain name\n * @param queueName - Queue name for scheduler callbacks\n */\n private async executeDownload(item: DownloadQueueItem, domain: string, queueName: string): Promise<void> {\n const notifier = AppContext.getNotifier();\n const registry = AppContext.getConfig();\n const { seriesUrl, episode, retryCount = 0 } = item;\n\n // Resolve config\n const resolvedConfig = registry.resolve(seriesUrl, 'series');\n const seriesName = resolvedConfig.name;\n const { downloadDelay } = resolvedConfig.download;\n\n try {\n // Attempt download\n await this.downloadManager.download(seriesUrl, episode);\n\n // Success - log and continue\n notifier.notify(\n NotificationLevel.SUCCESS,\n `[${domain}] Successfully queued download of Episode ${episode.number} for ${seriesName}`,\n );\n\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Check if we should retry\n const { maxRetries, initialTimeout, backoffMultiplier, jitterPercentage } = resolvedConfig.download;\n\n if (retryCount < maxRetries) {\n // Retry with backoff\n const retryDelay = this.calculateBackoff(\n retryCount,\n initialTimeout * 1000, // convert seconds to ms\n backoffMultiplier,\n jitterPercentage,\n );\n\n notifier.notify(\n NotificationLevel.WARNING,\n `[${domain}] Download failed for Episode ${episode.number}, retrying in ${Math.round(retryDelay / 1000)}s (attempt ${retryCount + 1}/${maxRetries})`,\n );\n\n // Requeue with incremented retry count\n const requeuedItem: DownloadQueueItem = {\n seriesUrl,\n episode,\n retryCount: retryCount + 1,\n };\n\n this.scheduler.addPriorityTask(queueName, requeuedItem, retryDelay);\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n } else {\n // Max retries exceeded - log error and give up\n notifier.notify(\n NotificationLevel.ERROR,\n `[${domain}] Failed to download Episode ${episode.number} after ${retryCount + 1} attempts: ${errorMessage}`,\n );\n this.scheduler.markTaskComplete(queueName, downloadDelay * 1000);\n }\n }\n }\n\n /**\n * Perform the actual check for new episodes\n *\n * @param handler - Domain handler\n * @param seriesUrl - Series URL\n * @param config - Resolved series configuration\n * @param attemptNumber - Current attempt number\n * @param domain - Domain name\n * @returns Check result\n */\n private async performCheck(\n handler: ReturnType<typeof handlerRegistry.getHandlerOrThrow>,\n seriesUrl: string,\n config: ResolvedConfig<'series'>,\n attemptNumber: number,\n domain: string,\n ): Promise<{ hasNewEpisodes: boolean; episodes: Episode[]; requeueDelay?: number }> {\n const notifier = AppContext.getNotifier();\n const seriesName = config.name;\n const checksCount = config.check.count;\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Checking ${seriesUrl} for new episodes... (attempt ${attemptNumber}/${checksCount})`,\n );\n\n // Extract episodes from the series page\n const episodes = await handler.extractEpisodes(seriesUrl);\n\n // Log episodes by type\n const episodesByType = new Map<EpisodeType, number>();\n episodes.forEach((ep) => {\n const count = episodesByType.get(ep.type) || 0;\n episodesByType.set(ep.type, count + 1);\n });\n\n const typeSummary = Array.from(episodesByType.entries())\n .map(([type, count]) => `${type}: ${count}`)\n .join(', ');\n\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Found ${episodes.length} total episodes on ${seriesUrl} (${typeSummary})`,\n );\n\n // Get download types from config\n const { downloadTypes } = config.check;\n\n // Filter for episodes matching download types and not yet downloaded\n const newEpisodes = episodes.filter((ep) => {\n const shouldDownload = downloadTypes.includes(ep.type as EpisodeType);\n // Get state path from config\n const statePath = config.stateFile;\n const notDownloaded = !this.stateManager.isDownloaded(statePath, seriesName, ep.number);\n return shouldDownload && notDownloaded;\n });\n\n // Log how many episodes will be downloaded\n if (episodes.length !== newEpisodes.length) {\n const skippedCount = episodes.length - newEpisodes.length;\n notifier.notify(\n NotificationLevel.INFO,\n `[${domain}] Filtering to ${downloadTypes.join(' or ')}: ${newEpisodes.length} episodes to download, ${skippedCount} skipped`,\n );\n }\n\n if (newEpisodes.length > 0) {\n return {\n hasNewEpisodes: true,\n episodes: newEpisodes,\n };\n }\n\n // No new episodes\n return {\n hasNewEpisodes: false,\n episodes: [],\n shouldRequeue: true,\n } as { hasNewEpisodes: false; episodes: Episode[]; requeueDelay?: number };\n }\n\n /**\n * Calculate exponential backoff with jitter\n *\n * @param retryCount - Current retry count\n * @param initialTimeout - Initial timeout in ms\n * @param backoffMultiplier - Multiplier for exponential backoff\n * @param jitterPercentage - Percentage of jitter (0-100)\n * @returns Delay in milliseconds\n */\n private calculateBackoff(\n retryCount: number,\n initialTimeout: number,\n backoffMultiplier: number,\n jitterPercentage: number,\n ): number {\n // Calculate base delay with exponential backoff\n const baseDelay = initialTimeout * backoffMultiplier ** retryCount;\n\n // Calculate jitter amount\n const jitterAmount = (baseDelay * jitterPercentage) / 100;\n\n // Generate random jitter within ±jitterAmount\n const jitter = (Math.random() * 2 - 1) * jitterAmount;\n\n // Calculate final delay (ensure non-negative)\n const finalDelay = Math.max(0, baseDelay + jitter);\n\n return Math.floor(finalDelay);\n }\n}\n","/**\n * TypedQueue - Passive queue for storing tasks of a single type\n *\n * A queue that manages tasks of a single type but does NOT execute them.\n * This is a passive data store that:\n * - Stores tasks in FIFO order\n * - Tracks cooldown time (next available timestamp)\n * - Tracks if a task of this type is currently executing\n * - Does NOT auto-start or have a processor function\n *\n * Key differences from AsyncQueue:\n * - No auto-start when items added\n * - No processor function (passive data store)\n * - Tracks cooldown from completion time\n * - No internal timing/sleep calls\n */\n\nexport type TaskItem<TaskType> = {\n data: TaskType;\n addedAt: Date;\n};\n\n/**\n * TypedQueue for a single task type\n */\nexport class TypedQueue<TaskType> {\n // State\n private queue: TaskItem<TaskType>[] = [];\n private isExecuting: boolean = false;\n private nextAvailableAt: Date = new Date(0); // Past = available\n private cooldownMs: number;\n\n /**\n * Create a new TypedQueue\n *\n * @param cooldownMs - Cooldown in milliseconds between task completions\n */\n constructor(cooldownMs: number = 0) {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Add a task to the queue\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n add(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.push({ data: task, addedAt });\n }\n\n /**\n * Add a task to the front of the queue (priority)\n *\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addFirst(task: TaskType, delay?: number): void {\n const addedAt = new Date(Date.now() + (delay ?? 0));\n this.queue.unshift({ data: task, addedAt });\n }\n\n /**\n * Get the next task from the queue\n *\n * @returns Next task or null if queue is empty\n */\n getNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n\n const item = this.queue.shift();\n return item?.data ?? null;\n }\n\n /**\n * Peek at the next task without removing it\n *\n * @returns Next task or null if queue is empty\n */\n peekNext(): TaskType | null {\n if (this.queue.length === 0) {\n return null;\n }\n return this.queue[0]?.data ?? null;\n }\n\n /**\n * Check if a task can start at the given time\n *\n * @param now - Current time\n * @returns Whether task can start\n */\n canStart(now: Date): boolean {\n if (this.isExecuting) {\n return false;\n }\n\n if (now < this.nextAvailableAt) {\n return false;\n }\n\n // Check if head task is ready (respect delay)\n const head = this.queue[0];\n if (head && now < head.addedAt) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Mark a task as started\n */\n markStarted(): void {\n this.isExecuting = true;\n }\n\n /**\n * Mark a task as completed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markCompleted(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Mark a task as failed and set cooldown\n *\n * @param cooldownMs - Cooldown in milliseconds from now\n */\n markFailed(cooldownMs: number): void {\n this.isExecuting = false;\n this.cooldownMs = cooldownMs;\n this.nextAvailableAt = new Date(Date.now() + cooldownMs);\n }\n\n /**\n * Check if queue has tasks\n *\n * @returns Whether queue has tasks\n */\n hasTasks(): boolean {\n return this.queue.length > 0;\n }\n\n /**\n * Get the next time this queue can start a task\n *\n * @returns Next available time\n */\n getNextAvailableTime(): Date {\n // Start with cooldown time\n let time = this.nextAvailableAt;\n\n // Check head task delay\n const head = this.queue[0];\n if (head && head.addedAt > time) {\n time = head.addedAt;\n }\n\n return time;\n }\n\n /**\n * Get queue length\n *\n * @returns Number of tasks in queue\n */\n getQueueLength(): number {\n return this.queue.length;\n }\n\n /**\n * Check if a task is currently executing\n *\n * @returns Whether a task is executing\n */\n getIsExecuting(): boolean {\n return this.isExecuting;\n }\n\n /**\n * Get cooldown duration\n *\n * @returns Cooldown in milliseconds\n */\n getCooldownMs(): number {\n return this.cooldownMs;\n }\n\n /**\n * Set cooldown duration\n *\n * @param cooldownMs - New cooldown in milliseconds\n */\n setCooldownMs(cooldownMs: number): void {\n this.cooldownMs = cooldownMs;\n }\n\n /**\n * Clear all tasks from the queue\n */\n clear(): void {\n this.queue = [];\n }\n\n /**\n * Get status information\n *\n * @returns Status object\n */\n getStatus(): {\n queueLength: number;\n isExecuting: boolean;\n nextAvailableAt: Date;\n cooldownMs: number;\n canStartNow: boolean;\n } {\n const now = new Date();\n return {\n queueLength: this.queue.length,\n isExecuting: this.isExecuting,\n nextAvailableAt: this.nextAvailableAt,\n cooldownMs: this.cooldownMs,\n canStartNow: this.canStart(now),\n };\n }\n}\n","/**\n * UniversalScheduler - Central scheduler for all typed queues\n *\n * Coordinates all typed queues with a single executor:\n * - Only one task executing globally\n * - Single active timer (cleared on scheduling attempt)\n * - Fair round-robin queue selection\n * - Event-driven (triggers on task add, completion, timer)\n *\n * Key features:\n * - Centralized scheduling logic\n * - Proper cooldowns (end-to-start timing)\n * - Reusable for any task type\n * - Timer-based instead of polling\n */\n\nimport { TypedQueue } from './typed-queue.js';\n\n/**\n * Executor callback function type\n */\nexport type ExecutorCallback<TaskType> = (task: TaskType, queueName: string) => Promise<void>;\n\n/**\n * Universal scheduler for coordinating all typed queues\n */\nexport class UniversalScheduler<TaskType> {\n // State\n private queues: Map<string, TypedQueue<TaskType>> = new Map();\n private queueCooldowns: Map<string, number> = new Map(); // Store default cooldown per queue\n private executorBusy: boolean = false;\n private timerId: ReturnType<typeof setTimeout> | null = null;\n private roundRobinIndex: number = 0;\n private stopped: boolean = false;\n\n // Callback\n private executor: ExecutorCallback<TaskType>;\n private onWait?: (queueName: string, waitMs: number, nextTime: Date) => void;\n\n /**\n * Create a new UniversalScheduler\n *\n * @param executor - Function to execute a task\n */\n constructor(executor: ExecutorCallback<TaskType>) {\n this.executor = executor;\n }\n\n /**\n * Set callback for when the scheduler is waiting\n *\n * @param callback - Callback function\n */\n setOnWait(callback: (queueName: string, waitMs: number, nextTime: Date) => void): void {\n this.onWait = callback;\n }\n\n /**\n * Register a new queue type\n *\n * @param typeName - Unique name for this queue type\n * @param cooldownMs - Default cooldown in milliseconds\n */\n registerQueue(typeName: string, cooldownMs: number): void {\n if (this.queues.has(typeName)) {\n throw new Error(`Queue ${typeName} is already registered`);\n }\n\n const queue = new TypedQueue<TaskType>(cooldownMs);\n this.queues.set(typeName, queue);\n this.queueCooldowns.set(typeName, cooldownMs);\n }\n\n /**\n * Check if a queue is registered\n *\n * @param typeName - Queue type name\n * @returns Whether queue is registered\n */\n hasQueue(typeName: string): boolean {\n return this.queues.has(typeName);\n }\n\n /**\n * Unregister a queue type\n *\n * @param typeName - Queue type name to unregister\n */\n unregisterQueue(typeName: string): void {\n this.queues.delete(typeName);\n this.queueCooldowns.delete(typeName);\n }\n\n /**\n * Add a task to a specific queue\n *\n * Triggers scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds before task is available\n */\n addTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.add(task, delay);\n\n // Trigger scheduling attempt (might be executable immediately)\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Add a priority task to the front of a specific queue\n *\n * @param typeName - Queue type name\n * @param task - Task to add\n * @param delay - Optional delay in milliseconds\n */\n addPriorityTask(typeName: string, task: TaskType, delay?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n queue.addFirst(task, delay);\n\n // Trigger scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as complete\n *\n * Called by executor when task completes successfully.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskComplete(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markCompleted(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Mark a task as failed\n *\n * Called by executor when task fails.\n * Triggers next scheduling attempt.\n *\n * @param typeName - Queue type name\n * @param cooldownMs - Optional cooldown override (uses queue default if not provided)\n */\n markTaskFailed(typeName: string, cooldownMs?: number): void {\n const queue = this.queues.get(typeName);\n if (!queue) {\n throw new Error(`Queue ${typeName} is not registered`);\n }\n\n const actualCooldown = cooldownMs ?? this.queueCooldowns.get(typeName) ?? 0;\n queue.markFailed(actualCooldown);\n this.executorBusy = false;\n\n // Trigger next scheduling attempt\n if (!this.stopped) {\n this.scheduleNext();\n }\n }\n\n /**\n * Schedule the next task\n *\n * Attempts to schedule immediately if possible,\n * otherwise sets a timer for the earliest available time.\n */\n scheduleNext(): void {\n if (this.stopped) {\n return;\n }\n\n // Clear any existing timer\n this.clearTimer();\n\n // Try to schedule immediately\n const scheduled = this.trySchedule();\n\n if (scheduled) {\n // Task scheduled and executor is busy.\n // No need to set timer, completion will trigger next schedule.\n return;\n }\n\n // If executor is busy but nothing new was scheduled (because it was already busy),\n // we also don't need a timer.\n if (this.executorBusy) {\n return;\n }\n\n // No task running and none could be scheduled.\n // Check if we should set a timer for the next available time\n const next = this.getEarliestAvailableTime();\n if (next) {\n const now = Date.now();\n const waitMs = Math.max(0, next.time.getTime() - now);\n this.scheduleTimer(waitMs, next.queueName, next.time);\n }\n }\n\n /**\n * Try to schedule a task now\n *\n * @returns Whether a task was scheduled\n */\n private trySchedule(): boolean {\n // Can't schedule if executor is busy\n if (this.executorBusy) {\n return false;\n }\n\n const now = new Date();\n\n // Collect queue names for round-robin\n const queueNames = Array.from(this.queues.keys());\n if (queueNames.length === 0) {\n return false;\n }\n\n // Try each queue in round-robin order\n for (let i = 0; i < queueNames.length; i++) {\n const index = (this.roundRobinIndex + i) % queueNames.length;\n const queueName = queueNames[index];\n if (!queueName) continue;\n\n const queue = this.queues.get(queueName);\n if (!queue) continue;\n\n // Check if queue has tasks and can start\n if (queue.hasTasks() && queue.canStart(now)) {\n // Get next task\n const task = queue.getNext();\n if (task) {\n // Mark as started\n queue.markStarted();\n this.executorBusy = true;\n this.roundRobinIndex = (index + 1) % queueNames.length;\n\n // Execute task (fire and forget - executor will call back)\n this.executeTask(queueName, task).catch((error) => {\n // Execution failed - mark as failed and continue\n console.error(`[UniversalScheduler] Task execution failed: ${error}`);\n this.markTaskFailed(queueName);\n });\n\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Execute a task\n *\n * @param queueName - Queue name\n * @param task - Task to execute\n */\n private async executeTask(queueName: string, task: TaskType): Promise<void> {\n await this.executor(task, queueName);\n }\n\n /**\n * Schedule a timer for the next attempt\n *\n * @param waitMs - Milliseconds to wait\n * @param queueName - Name of the queue we are waiting for\n * @param nextTime - Time when the task will be ready\n */\n private scheduleTimer(waitMs: number, queueName: string, nextTime: Date): void {\n this.clearTimer();\n\n // Notify waiting state if callback defined and wait is significant (>1s)\n if (this.onWait && waitMs > 1000) {\n this.onWait(queueName, waitMs, nextTime);\n }\n\n this.timerId = setTimeout(() => {\n this.timerId = null;\n this.scheduleNext();\n }, waitMs);\n }\n\n /**\n * Clear the active timer\n */\n private clearTimer(): void {\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n }\n\n /**\n * Get the earliest available time across all queues\n *\n * @returns Earliest available time and queue name, or null if no queues with tasks\n */\n private getEarliestAvailableTime(): { time: Date; queueName: string } | null {\n let result: { time: Date; queueName: string } | null = null;\n\n for (const [name, queue] of this.queues.entries()) {\n // Only consider queues that have tasks\n if (!queue.hasTasks()) {\n continue;\n }\n\n const nextTime = queue.getNextAvailableTime();\n if (result === null || nextTime < result.time) {\n result = { time: nextTime, queueName: name };\n }\n }\n\n return result;\n }\n\n /**\n * Stop the scheduler\n *\n * Clears timers and prevents further scheduling.\n */\n stop(): void {\n this.stopped = true;\n this.clearTimer();\n }\n\n /**\n * Resume the scheduler\n */\n resume(): void {\n this.stopped = false;\n this.scheduleNext();\n }\n\n /**\n * Get statistics for all queues\n *\n * @returns Map of queue name to status\n */\n getStats(): Map<string, { queueLength: number; isExecuting: boolean; nextAvailableAt: Date }> {\n const stats = new Map();\n\n for (const [name, queue] of this.queues.entries()) {\n const status = queue.getStatus();\n stats.set(name, {\n queueLength: status.queueLength,\n isExecuting: status.isExecuting,\n nextAvailableAt: status.nextAvailableAt,\n });\n }\n\n return stats;\n }\n\n /**\n * Check if executor is busy\n *\n * @returns Whether executor is busy\n */\n isExecutorBusy(): boolean {\n return this.executorBusy;\n }\n\n /**\n * Check if there are any pending tasks\n *\n * @returns Whether there are pending tasks\n */\n hasPendingTasks(): boolean {\n for (const queue of this.queues.values()) {\n if (queue.hasTasks()) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get total pending tasks across all queues\n *\n * @returns Total pending task count\n */\n getTotalPendingTasks(): number {\n let total = 0;\n for (const queue of this.queues.values()) {\n total += queue.getQueueLength();\n }\n return total;\n }\n}\n","import parser from 'cron-parser';\n\n/**\n * Parse time string in HH:MM format\n *\n * @param timeStr - Time string in HH:MM format (e.g., \"20:00\")\n * @returns Date object with today's date and the specified time\n */\nexport function parseTime(timeStr: string): Date {\n const match = timeStr.match(/^(\\d{1,2}):(\\d{2})$/);\n if (!match) {\n throw new Error(`Invalid time format: \"${timeStr}\". Expected HH:MM`);\n }\n\n const [, hoursStr, minutesStr] = match;\n const hours = parseInt(hoursStr || '0', 10);\n const minutes = parseInt(minutesStr || '0', 10);\n\n if (hours < 0 || hours > 23) {\n throw new Error(`Invalid hours: ${hours}. Must be between 0 and 23`);\n }\n\n if (minutes < 0 || minutes > 59) {\n throw new Error(`Invalid minutes: ${minutes}. Must be between 0 and 59`);\n }\n\n const date = new Date();\n date.setHours(hours, minutes, 0, 0);\n return date;\n}\n\n/**\n * Get milliseconds until the next occurrence of a time\n *\n * @param timeStr - Time string in HH:MM format\n * @returns Milliseconds until the next occurrence\n */\nexport function getMsUntilTime(timeStr: string): number {\n const targetTime = parseTime(timeStr);\n const now = new Date();\n\n const targetDate = new Date(now);\n targetDate.setHours(targetTime.getHours(), targetTime.getMinutes(), 0, 0);\n\n // If the time has already passed today, schedule for tomorrow\n if (targetDate <= now) {\n targetDate.setDate(targetDate.getDate() + 1);\n }\n\n return targetDate.getTime() - now.getTime();\n}\n\n/**\n * Get milliseconds until the next occurrence of a cron expression\n *\n * @param cronExpression - Cron expression\n * @returns Milliseconds until the next occurrence\n */\nexport function getMsUntilCron(cronExpression: string): number {\n try {\n const interval = parser.parseExpression(cronExpression);\n const nextDate = interval.next().toDate();\n const now = new Date();\n return nextDate.getTime() - now.getTime();\n } catch (err) {\n throw new Error(\n `Invalid cron expression: \"${cronExpression}\". Error: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Format duration in human-readable format\n *\n * @param ms - Duration in milliseconds\n * @returns Formatted duration string\n */\nexport function formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d ${hours % 24}h`;\n }\n if (hours > 0) {\n return `${hours}h ${minutes % 60}m`;\n }\n if (minutes > 0) {\n return `${minutes}m ${seconds % 60}s`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Sleep for specified milliseconds\n *\n * @param ms - Milliseconds to sleep\n * @returns Promise that resolves after the sleep\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Scheduler - Queue-based architecture for managing series checks\n *\n * Features:\n * - Per-domain sequential processing (concatMap semantics)\n * - Domain-based parallelism\n * - Retry with exponential backoff\n * - \"No episodes\" requeue with interval\n * - Graceful shutdown\n */\n\nimport { AppContext } from '../app-context.js';\nimport type { SeriesConfig } from '../config/config-schema.js';\nimport type { DownloadManager } from '../downloader/download-manager.js';\nimport { SchedulerError } from '../errors/custom-errors.js';\nimport { NotificationLevel } from '../notifications/notifier.js';\nimport { QueueManager } from '../queue/queue-manager.js';\nimport type { SchedulerOptions } from '../types/config.types.js';\nimport { getMsUntilCron, getMsUntilTime, sleep } from '../utils/time-utils.js';\n\n/**\n * Time provider type for dependency injection\n */\nexport type TimeProvider = {\n getMsUntilTime: typeof getMsUntilTime;\n getMsUntilCron: typeof getMsUntilCron;\n sleep: typeof sleep;\n};\n\n/**\n * QueueManager factory type for dependency injection\n */\nexport type QueueManagerFactory = (downloadManager: DownloadManager) => QueueManager;\n\n/**\n * Scheduler for managing periodic checks with queue-based architecture\n */\nexport class Scheduler {\n private configs: SeriesConfig[];\n private downloadManager: DownloadManager;\n private options: SchedulerOptions;\n private queueManager: QueueManager;\n private running: boolean = false;\n private stopped: boolean = true;\n private timeProvider: TimeProvider;\n private scheduleTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n configs: SeriesConfig[],\n downloadManager: DownloadManager,\n options: SchedulerOptions = { mode: 'scheduled' },\n timeProvider?: TimeProvider,\n queueManagerFactory?: QueueManagerFactory,\n ) {\n this.configs = configs;\n this.downloadManager = downloadManager;\n this.options = options;\n this.timeProvider = timeProvider || { getMsUntilTime, getMsUntilCron, sleep };\n\n // Create queue manager\n const createQueueManager = queueManagerFactory || ((dm) => new QueueManager(dm));\n\n this.queueManager = createQueueManager(this.downloadManager);\n }\n\n /**\n * Start the scheduler\n */\n async start(): Promise<void> {\n if (this.running) {\n throw new SchedulerError('Scheduler is already running');\n }\n\n this.running = true;\n this.stopped = false;\n\n // Start queue manager\n this.queueManager.start();\n\n const notifier = AppContext.getNotifier();\n\n if (this.options.mode === 'once') {\n notifier.notify(NotificationLevel.INFO, 'Single-run mode: checking all series once');\n await this.runOnce();\n this.running = false;\n } else {\n notifier.notify(NotificationLevel.INFO, 'Scheduler started (queue-based architecture)');\n this.scheduleNextBatch();\n\n // Keep promise pending forever for scheduled mode to prevent process exit\n // In a real app, this is handled by the event loop being active (timers/intervals)\n // but runApp awaits start(), so we return a promise that only resolves on stop()\n return new Promise<void>((resolve) => {\n const checkStop = setInterval(() => {\n if (!this.running) {\n clearInterval(checkStop);\n resolve();\n }\n }, 100);\n });\n }\n }\n\n private scheduleNextBatch(): void {\n if (this.stopped) return;\n\n const notifier = AppContext.getNotifier();\n const groupedConfigs = this.groupConfigsBySchedule();\n let nextScheduleKey: string | null = null;\n let minMsUntil = Number.MAX_SAFE_INTEGER;\n\n for (const scheduleKey of groupedConfigs.keys()) {\n let msUntil: number;\n\n try {\n // Determine if it's HH:MM or cron\n if (/^\\d{1,2}:\\d{2}$/.test(scheduleKey)) {\n msUntil = this.timeProvider.getMsUntilTime(scheduleKey);\n } else {\n // Assume cron\n msUntil = this.timeProvider.getMsUntilCron(scheduleKey);\n }\n\n if (msUntil < minMsUntil) {\n minMsUntil = msUntil;\n nextScheduleKey = scheduleKey;\n }\n } catch (error) {\n notifier.notify(\n NotificationLevel.ERROR,\n `Error calculating next run time for schedule \"${scheduleKey}\": ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n if (!nextScheduleKey) {\n notifier.notify(NotificationLevel.WARNING, 'No scheduled configs found.');\n return;\n }\n\n const configs = groupedConfigs.get(nextScheduleKey);\n if (!configs) return;\n\n if (minMsUntil > 0) {\n this.options.onIdle?.();\n notifier.notify(\n NotificationLevel.INFO,\n `Waiting ${Math.floor(minMsUntil / 1000 / 60)} minutes until next run (${nextScheduleKey})...`,\n );\n }\n\n // Schedule next run\n this.scheduleTimer = setTimeout(async () => {\n if (this.stopped) return;\n await this.runConfigs(configs);\n await this.waitForQueueDrain();\n this.scheduleNextBatch();\n }, minMsUntil);\n }\n\n /**\n * Wait for all queues to drain\n */\n private async waitForQueueDrain(): Promise<void> {\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n }\n\n /**\n * Stop the scheduler\n */\n async stop(): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Stopping scheduler...');\n\n this.stopped = true;\n if (this.scheduleTimer) {\n clearTimeout(this.scheduleTimer);\n this.scheduleTimer = null;\n }\n\n // Stop queue manager (drains all queues)\n await this.queueManager.stop();\n\n this.running = false;\n\n notifier.notify(NotificationLevel.INFO, 'Scheduler stopped');\n }\n\n /**\n * Reload configuration\n */\n async reload(configs: SeriesConfig[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Reloading configuration...');\n\n // Update internal state\n this.configs = configs;\n\n // Update queue manager config (reloads from AppContext)\n this.queueManager.updateConfig();\n\n // If running in scheduled mode, restart the schedule\n if (this.running && this.options.mode === 'scheduled') {\n if (this.scheduleTimer) {\n clearTimeout(this.scheduleTimer);\n this.scheduleTimer = null;\n }\n this.scheduleNextBatch();\n }\n }\n\n /**\n * Update the download manager instance\n * Used during config reload when download settings change\n */\n updateDownloadManager(downloadManager: DownloadManager): void {\n this.downloadManager = downloadManager;\n // QueueManager gets DownloadManager through AppContext, no need to update\n }\n\n /**\n * Trigger immediate checks for all series\n */\n async triggerAllChecks(): Promise<void> {\n const notifier = AppContext.getNotifier();\n notifier.notify(NotificationLevel.INFO, 'Triggering immediate checks for all series...');\n for (const config of this.configs) {\n this.queueManager.addSeriesCheck(config.url);\n }\n }\n\n /**\n * Group configs by schedule (startTime or cron)\n */\n private groupConfigsBySchedule(): Map<string, SeriesConfig[]> {\n const notifier = AppContext.getNotifier();\n const grouped = new Map<string, SeriesConfig[]>();\n\n for (const config of this.configs) {\n const scheduleKey = config.cron || config.startTime;\n if (!scheduleKey) {\n notifier.notify(\n NotificationLevel.WARNING,\n `Series \"${config.name}\" has no startTime or cron configured. Skipping.`,\n );\n continue;\n }\n\n const existing = grouped.get(scheduleKey) || [];\n existing.push(config);\n grouped.set(scheduleKey, existing);\n }\n\n return grouped;\n }\n\n /**\n * Add all configs to queue manager\n */\n private async runConfigs(configs: SeriesConfig[]): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n // Add all series to the queue manager\n for (const config of configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config.url);\n }\n\n // Log queue stats\n const stats = this.queueManager.getQueueStats();\n notifier.notify(\n NotificationLevel.INFO,\n `Added ${configs.length} series to check queues. Queue stats: ${JSON.stringify(stats)}`,\n );\n }\n\n /**\n * Run all configs in single-run mode\n */\n private async runOnce(): Promise<void> {\n const notifier = AppContext.getNotifier();\n\n for (const config of this.configs) {\n if (this.stopped) break;\n\n this.queueManager.addSeriesCheck(config.url);\n }\n\n // Wait for all queues to drain\n while (this.queueManager.hasActiveProcessing()) {\n if (this.stopped) break;\n // biome-ignore lint/performance/noAwaitInLoops: Sequential polling is intentional\n await this.timeProvider.sleep(1000);\n }\n\n notifier.notify(NotificationLevel.SUCCESS, 'Single-run complete');\n }\n\n /**\n * Check if scheduler is running\n */\n isRunning(): boolean {\n return this.running && !this.stopped;\n }\n\n /**\n * Get queue manager (for testing/debugging)\n */\n getQueueManager(): QueueManager {\n return this.queueManager;\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { CookieError } from '../errors/custom-errors';\n\n/**\n * Get browser cookie database path\n */\nfunction getBrowserPath(browser: string): string {\n const home = homedir();\n const platform = process.platform;\n\n const paths: Record<string, Record<string, string>> = {\n darwin: {\n chrome: join(home, 'Library/Application Support/Google/Chrome/Default/Cookies'),\n chromium: join(home, 'Library/Application Support/Chromium/Default/Cookies'),\n edge: join(home, 'Library/Application Support/Microsoft Edge/Default/Cookies'),\n firefox: join(home, 'Library/Application Support/Firefox/Profiles'),\n safari: join(home, 'Library/Cookies/Cookies.binarycookies'),\n },\n linux: {\n chrome: join(home, '.config/google-chrome/Default/Cookies'),\n chromium: join(home, '.config/chromium/Default/Cookies'),\n edge: join(home, '.config/microsoft-edge/Default/Cookies'),\n firefox: join(home, '.mozilla/firefox'),\n safari: '', // Safari not on Linux\n },\n win32: {\n chrome: join(home, 'AppData/Local/Google/Chrome/User Data/Default/Cookies'),\n chromium: join(home, 'AppData/Local/Chromium/User Data/Default/Cookies'),\n edge: join(home, 'AppData/Local/Microsoft/Edge/User Data/Default/Cookies'),\n firefox: join(home, 'AppData/Roaming/Mozilla/Firefox/Profiles'),\n safari: '', // Safari not on Windows\n },\n };\n\n const browserPaths = paths[platform];\n if (!browserPaths) {\n throw new CookieError(`Unsupported platform: ${platform}`);\n }\n\n const path = browserPaths[browser];\n if (!path) {\n throw new CookieError(`Browser \"${browser}\" not supported on ${platform}`);\n }\n\n return path;\n}\n\n/**\n * Extract cookies from browser for a specific domain\n * This is a simplified version - in production, you'd use a proper SQLite parser\n * or a library like `tough-cookie-file-store`\n *\n * @param domain - Domain to extract cookies for (e.g., \"wetv.vip\")\n * @param browser - Browser to extract from\n * @returns Cookie string in Netscape format\n */\nexport async function extractCookies(domain: string, browser: string = 'chrome'): Promise<string> {\n const cookiePath = getBrowserPath(browser);\n\n if (!existsSync(cookiePath)) {\n throw new CookieError(\n `Cookie database not found at \"${cookiePath}\". ` +\n `Make sure ${browser} is installed and you've logged in to the site.`,\n );\n }\n\n // For now, we'll use a simpler approach: tell the user to export cookies manually\n // In production, you'd use a proper SQLite parser here\n throw new CookieError(\n `Automatic cookie extraction is not yet implemented for ${browser}. ` +\n `Please export cookies manually:\\n` +\n `1. Install a browser extension like \"Get cookies.txt LOCALLY\"\\n` +\n `2. Go to ${domain} and log in\\n` +\n `3. Export cookies to a file\\n` +\n `4. Set 'cookieFile' in config.yaml to the exported file path`,\n );\n}\n\n/**\n * Read cookies from a Netscape-format cookie file\n *\n * @param cookieFile - Path to cookie file\n * @returns Cookie string for HTTP requests\n */\nexport async function readCookieFile(cookieFile: string): Promise<string> {\n if (!existsSync(cookieFile)) {\n throw new CookieError(`Cookie file not found: \"${cookieFile}\"`);\n }\n\n const content = await readFile(cookieFile, 'utf-8');\n\n // Parse Netscape cookie format and convert to Cookie header format\n const lines = content.split('\\n');\n const cookies: string[] = [];\n\n for (const line of lines) {\n // Skip comments and empty lines\n const trimmedLine = line.trim();\n if (trimmedLine.startsWith('#') || !trimmedLine) continue;\n\n const fields = line.split('\\t');\n if (fields.length >= 7) {\n const name = fields[5];\n const value = fields[6];\n\n if (name && value) {\n const cleanValue = value.trim();\n if (cleanValue) {\n cookies.push(`${name}=${cleanValue}`);\n }\n }\n }\n }\n\n return cookies.join('; ');\n}\n"],"mappings":"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","names":["fileURLToPath","run","readline","boolean","command","flag","option","string","existsSync","readFileSync","writeFile","isAbsolute","join","createEmptyState","StateManager","_StateManager","notifier","statePath","seriesName","episodeNumber","episodes","paddedNumber","error","state","episodeStr","fn","currentLock","lockPromise","fullPath","existsSync","createEmptyState","fileContent","readFileSync","sortedSeries","key","content","writeFile","isAbsolute","join","message","errorMessage","fullMessage","AppContext","_AppContext","configRegistry","notifier","stateManager","StateManager","existsSync","readFile","join","yaml","WetvloError","message","ConfigError","HandlerError","WetvloError","message","url","DownloadError","NotificationError","CookieError","SchedulerError","resolveEnv","value","_match","varName","envValue","resolveEnvRecursive","obj","item","result","key","z","EpisodeTypeSchema","CheckSettingsSchema","DownloadSettingsSchema","TelegramConfigSchema","BrowserSchema","CommonSettingsSchema","GlobalConfigSchema","DomainConfigSchema","SeriesConfigSchema","ConfigSchema","validateConfigWithWarnings","rawConfig","warnings","globalConfig","domainConfig","index","dc","warning","DEFAULT_CONFIG_PATH","loadConfig","configPath","absolutePath","join","existsSync","ConfigError","content","readFile","rawConfig","error","validateConfigWithWarnings","resolveEnvRecursive","deepMerge","target","source","result","key","sourceValue","targetValue","isObject","item","extractDomain","url","defaults","getDefaults","ConfigRegistry","root","defaults","getDefaults","globalMerged","deepMerge","dc","domainMerged","sc","hostname","extractDomain","globalConfig","seriesMerged","key","config","url","level","domain","resolved","domains","check","download","fs","fsPromises","basename","join","resolve","sanitizeFilename","name","execa","colors","Logger","config","level","date","month","day","hour","min","sec","message","timestamp","emoji","text","color","levels","logger","getVideoDuration","filePath","stdout","execa","duration","error","logger","extractDownloadOptions","resolvedConfig","BaseDownloader","fsPromises","join","execa","YtdlpWrapper","url","outputName","dir","options","args","cookieFile","subLangs","onProgress","onLog","outputTemplate","cmdArgs","arg","filename","allFiles","outputBuffer","subprocess","line","text","destMatch","subMatch","mergeMatch","progressMatch","percentage","totalSize","speed","eta","error","errorMsg","fullLog","YtDlpDownloader","BaseDownloader","YtdlpWrapper","_url","episode","dir","filenameWithoutExt","options","DownloaderRegistry","YtDlpDownloader","downloader","url","downloaderRegistry","escapeRegExp","string","DownloadManager","AppContext","seriesUrl","episode","notifier","resolved","statePath","seriesName","downloadOptions","extractDownloadOptions","downloader","downloaderRegistry","paddedNumber","filenameWithoutExt","sanitizeFilename","targetDir","result","progress","message","fileSize","fullPath","resolve","duration","getVideoDuration","file","absFile","fileName","basename","newPath","join","e","error","DownloadError","files","dir","absDir","pattern","cleanedCount","filePath","filename","bytes","units","size","unit","YtDlpDownloader","Registry","handler","url","domain","extractDomain","handlerDomain","HandlerError","handlerRegistry","cheerio","BaseHandler","url","domain","extractDomain","cookies","headers","response","HandlerError","error","html","text","chineseMatch","epMatch","episodeMatch","numberMatch","element","$","$el","className","IQiyiHandler","BaseHandler","url","cookies","html","nextDataEpisodes","episodes","match","dataStr","pageData","albumInfo","videoList","albumId","title","video","vid","episode","isTrailer","payStatus","episodeNumber","episodeUrl","type","error","a","b","$","selectors","selector","links","_","element","$el","href","text","ep","$parent","MGTVHandler","BaseHandler","url","cookies","videoId","episodes","page","totalPages","apiUrl","response","data","item","episodeNumber","episodeUrl","match","uniqueEpisodes","episode","a","b","WeTVHandler","BaseHandler","url","cookies","html","nextDataEpisodes","episodes","match","dataStr","pageData","coverInfo","videoList","cid","title","coverId","video","vid","episode","isTrailer","episodeNumber","encodedTitle","episodeUrl","type","error","a","b","$","episodeLinks","_","el","element","labels","payStatus","defaultPayStatus","key","label","labelText","status","$el","href","ariaLabel","ep","$li","badge","badgeText","liText","chromium","BLOCKED_RESOURCE_TYPES","YoukuHandler","BaseHandler","url","cookies","match","browser","chromium","context","page","apiResponses","response","requestUrl","body","logger","e","route","resourceType","data","nodes","initialInfo","episodeComponent","episodeNodes","lastStage","startTime","maxWait","currentInfo","initialData","episodes","apiResponse","additionalEpisodes","existingIds","ep","a","b","keys","key","value","item","stage","title","paid","videoId","episodeUrl","type","error","ConsoleNotifier","level","message","logger","TelegramNotifier","config","level","message","emoji","MAX_LENGTH","safeMessage","escapedMessage","formattedMessage","response","errorText","NotificationError","error","text","_message","createHash","TypedQueue","cooldownMs","task","delay","addedAt","now","head","time","UniversalScheduler","executor","callback","typeName","cooldownMs","queue","TypedQueue","task","delay","actualCooldown","next","now","waitMs","queueNames","i","index","queueName","error","nextTime","result","name","stats","status","total","QueueManager","downloadManager","schedulerFactory","AppContext","createScheduler","executor","UniversalScheduler","task","queueName","waitMs","notifier","seconds","parts","type","domain","seriesUrl","registry","extractDomain","seriesName","item","episodes","resolvedConfig","downloadDelay","i","episode","delayMs","stats","checkQueues","downloadQueues","queueStats","testUrl","hash","createHash","checkInterval","handler","handlerRegistry","attemptNumber","retryCount","checksCount","result","intervalMs","requeueDelay","requeuedItem","error","errorMessage","maxRetries","initialTimeout","backoffMultiplier","jitterPercentage","retryDelay","config","episodesByType","ep","count","typeSummary","downloadTypes","newEpisodes","shouldDownload","statePath","notDownloaded","skippedCount","baseDelay","jitterAmount","jitter","finalDelay","parser","parseTime","timeStr","match","hoursStr","minutesStr","hours","minutes","date","getMsUntilTime","targetTime","now","targetDate","getMsUntilCron","cronExpression","nextDate","err","sleep","ms","resolve","Scheduler","configs","downloadManager","options","timeProvider","queueManagerFactory","getMsUntilTime","getMsUntilCron","sleep","createQueueManager","dm","QueueManager","SchedulerError","notifier","AppContext","resolve","checkStop","groupedConfigs","nextScheduleKey","minMsUntil","scheduleKey","msUntil","error","config","grouped","existing","stats","existsSync","readFile","homedir","join","readCookieFile","cookieFile","existsSync","CookieError","lines","readFile","cookies","line","trimmedLine","fields","name","value","cleanValue","defaultDependencies","loadConfig","DownloadManager","readCookieFile","c","dm","opt","Scheduler","handleShutdown","scheduler","logger","error","runApp","configPath","mode","deps","config","configRegistry","ConfigRegistry","_globalConfig","createNotifier","registry","notifiers","ConsoleNotifier","cfg","TelegramNotifier","level","message","n","notifier","stateManager","StateManager","AppContext","handlerRegistry","WeTVHandler","IQiyiHandler","MGTVHandler","YoukuHandler","downloadManager","onIdle","str","key","name","char","newConfig","newConfigRegistry","newGlobalConfig","newNotifier","cli","command","option","string","flag","boolean","once","ConfigError","main","args","run","cli","isMainModule","fileURLToPath"]}