205 lines
5.6 KiB
TypeScript
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;
|
|
}
|