import cron from "node-cron"; import { videoOperations, configOperations, channelOperations, } from "../db/database"; import { downloadVideo, getChannelVideos } from "./ytdlp"; import { DownloadProgress } from "../types"; let schedulerTask: cron.ScheduledTask | null = null; let isDownloading = false; let currentProgress: DownloadProgress | null = null; let nextScheduledDownload: Date | null = null; function getRandomVariance(varianceMinutes: number): number { return Math.floor(Math.random() * varianceMinutes * 2) - varianceMinutes; } function calculateNextRun( intervalMinutes: number, varianceMinutes: number, ): Date { const variance = getRandomVariance(varianceMinutes); const totalMinutes = intervalMinutes + variance; const nextRun = new Date(); nextRun.setMinutes(nextRun.getMinutes() + totalMinutes); return nextRun; } async function downloadNextVideo() { if (isDownloading) { console.log("Already downloading a video, skipping..."); return; } isDownloading = true; currentProgress = null; const video = videoOperations.getNextPending(); if (!video) { console.log("No pending videos to download"); return; } try { console.log(`Starting download: ${video.title} (${video.videoId})`); videoOperations.updateStatus(video.id, "downloading"); const result = await downloadVideo(video.videoId, video.url, { onProgress: (progress, speed, eta) => { currentProgress = { videoId: video.videoId, progress, speed, eta, }; }, }); videoOperations.markCompleted(video.id, result.filePath, result.fileSize); console.log(`Download completed: ${video.title}`); currentProgress = null; } catch (error) { console.error("Download error:", error); if (video) { videoOperations.updateStatus( video.id, "failed", error instanceof Error ? error.message : "Unknown error", ); } currentProgress = null; } finally { isDownloading = false; } } async function checkChannelsForNewVideos() { const channels = channelOperations.getAll().filter((c) => c.active); for (const channel of channels) { try { console.log(`Checking channel: ${channel.name}`); const videos = await getChannelVideos(channel.url); for (const videoInfo of videos) { // Check if video already exists const existingVideos = videoOperations.getAll(); const exists = existingVideos.some((v) => v.videoId === videoInfo.id); if (!exists) { console.log(`Found new video: ${videoInfo.title}`); videoOperations.create({ channelId: channel.id, playlistId: null, videoId: videoInfo.id, title: videoInfo.title, url: videoInfo.url, duration: videoInfo.duration, thumbnail: videoInfo.thumbnail, uploadDate: videoInfo.uploadDate, status: "pending", filePath: null, fileSize: null, error: null, }); } } channelOperations.updateLastChecked(channel.id); } catch (error) { console.error(`Error checking channel ${channel.name}:`, error); } } } export function startScheduler() { if (schedulerTask) { console.log("Scheduler already running"); return; } const config = configOperations.getAll(); const enabled = config.enabled === "true"; if (!enabled) { console.log("Scheduler is disabled"); return; } const intervalMinutes = parseFloat(config.intervalMinutes || "180"); // Default 180 minutes (3 hours) const varianceMinutes = parseFloat(config.varianceMinutes || "30"); console.log( `Starting scheduler: ${intervalMinutes}min ±${varianceMinutes}min`, ); // Check for new videos every hour schedulerTask = cron.schedule("0 * * * *", async () => { console.log("Checking channels for new videos..."); await checkChannelsForNewVideos(); }); // Download next video with variable interval async function scheduleNextDownload() { const config = configOperations.getAll(); const enabled = config.enabled === "true"; if (!enabled) { console.log("Scheduler disabled, stopping..."); return; } // Download video first (this may take a long time) await downloadNextVideo(); // AFTER download completes, calculate next run const intervalMinutes = parseFloat(config.intervalMinutes || "180"); // Default 180 minutes (3 hours) const varianceMinutes = parseFloat(config.varianceMinutes || "30"); const nextRun = calculateNextRun(intervalMinutes, varianceMinutes); nextScheduledDownload = nextRun; console.log(`Next download scheduled for: ${nextRun.toLocaleString()}`); const delay = nextRun.getTime() - Date.now(); setTimeout(scheduleNextDownload, delay); } // Start the first download cycle scheduleNextDownload(); console.log("Scheduler started"); } export function stopScheduler() { if (schedulerTask) { schedulerTask.stop(); schedulerTask = null; console.log("Scheduler stopped"); } } export function restartScheduler() { stopScheduler(); startScheduler(); } export function getSchedulerStatus() { const config = configOperations.getAll(); return { running: schedulerTask !== null, enabled: config.enabled === "true", intervalMinutes: parseFloat(config.intervalMinutes || "180"), varianceMinutes: parseFloat(config.varianceMinutes || "30"), isDownloading, currentProgress, nextScheduledDownload: nextScheduledDownload?.toISOString() || null, }; } export function getCurrentProgress(): DownloadProgress | null { return currentProgress; }