vidrip/backend/src/services/scheduler.ts

205 lines
5.6 KiB
TypeScript

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;
}