const db_api = require('./db'); const youtubedl_api = require('./youtube-dl'); const fs = require('fs-extra'); const logger = require('./logger'); const scheduler = require('node-schedule'); const TASKS = { backup_local_db: { run: db_api.backupDB, title: 'Backup DB', job: null }, missing_files_check: { run: checkForMissingFiles, confirm: deleteMissingFiles, title: 'Missing files check', job: null }, missing_db_records: { run: db_api.importUnregisteredFiles, title: 'Import missing DB records', job: null }, duplicate_files_check: { run: checkForDuplicateFiles, confirm: removeDuplicates, title: 'Find duplicate files in DB', job: null }, youtubedl_update_check: { run: youtubedl_api.checkForYoutubeDLUpdate, confirm: youtubedl_api.updateYoutubeDL, title: 'Update youtube-dl', job: null } } function scheduleJob(task_key, schedule) { // schedule has to be converted from our format to one node-schedule can consume let converted_schedule = null; if (schedule['type'] === 'timestamp') { converted_schedule = new Date(schedule['data']['timestamp']); } else if (schedule['type'] === 'recurring') { const dayOfWeek = schedule['data']['dayOfWeek'] != null ? schedule['data']['dayOfWeek'] : null; const hour = schedule['data']['hour'] != null ? schedule['data']['hour'] : null; const minute = schedule['data']['minute'] != null ? schedule['data']['minute'] : null; converted_schedule = new scheduler.RecurrenceRule(null, null, null, dayOfWeek, hour, minute); } else { logger.error(`Failed to schedule job '${task_key}' as the type '${schedule['type']}' is invalid.`) return null; } return scheduler.scheduleJob(converted_schedule, async () => { const task_state = await db_api.getRecord('tasks', {key: task_key}); if (task_state['running'] || task_state['confirming']) { logger.verbose(`Skipping running task ${task_state['key']} as it is already in progress.`); return; } // remove schedule if it's a one-time task if (task_state['schedule']['type'] !== 'recurring') await db_api.updateRecord('tasks', {key: task_key}, {schedule: null}); // we're just "running" the task, any confirmation should be user-initiated exports.executeRun(task_key); }); } if (db_api.database_initialized) { exports.setupTasks(); } else { db_api.database_initialized_bs.subscribe(init => { if (init) exports.setupTasks(); }); } exports.setupTasks = async () => { const tasks_keys = Object.keys(TASKS); for (let i = 0; i < tasks_keys.length; i++) { const task_key = tasks_keys[i]; const task_in_db = await db_api.getRecord('tasks', {key: task_key}); if (!task_in_db) { // insert task metadata into table if missing await db_api.insertRecordIntoTable('tasks', { key: task_key, title: TASKS[task_key]['title'], last_ran: null, last_confirmed: null, running: false, confirming: false, data: null, error: null, schedule: null, options: {} }); } else { // reset task if necessary await db_api.updateRecord('tasks', {key: task_key}, {running: false, confirming: false}); // schedule task and save job if (task_in_db['schedule']) { // prevent timestamp schedules from being set to the past if (task_in_db['schedule']['type'] === 'timestamp' && task_in_db['schedule']['data']['timestamp'] < Date.now()) { await db_api.updateRecord('tasks', {key: task_key}, {schedule: null}); continue; } TASKS[task_key]['job'] = scheduleJob(task_key, task_in_db['schedule']); } } } } exports.executeTask = async (task_key) => { if (!TASKS[task_key]) { logger.error(`Task ${task_key} does not exist!`); return; } logger.verbose(`Executing task ${task_key}`); await exports.executeRun(task_key); if (!TASKS[task_key]['confirm']) return; await exports.executeConfirm(task_key); logger.verbose(`Finished executing ${task_key}`); } exports.executeRun = async (task_key) => { logger.verbose(`Running task ${task_key}`); // don't set running to true when backup up DB as it will be stick "running" if restored if (task_key !== 'backup_local_db') await db_api.updateRecord('tasks', {key: task_key}, {running: true}); const data = await TASKS[task_key].run(); await db_api.updateRecord('tasks', {key: task_key}, {data: TASKS[task_key]['confirm'] ? data : null, last_ran: Date.now()/1000, running: false}); logger.verbose(`Finished running task ${task_key}`); } exports.executeConfirm = async (task_key) => { logger.verbose(`Confirming task ${task_key}`); if (!TASKS[task_key]['confirm']) { return null; } await db_api.updateRecord('tasks', {key: task_key}, {confirming: true}); const task_obj = await db_api.getRecord('tasks', {key: task_key}); const data = task_obj['data']; await TASKS[task_key].confirm(data); await db_api.updateRecord('tasks', {key: task_key}, {confirming: false, last_confirmed: Date.now()/1000, data: null}); logger.verbose(`Finished confirming task ${task_key}`); } exports.updateTaskSchedule = async (task_key, schedule) => { logger.verbose(`Updating schedule for task ${task_key}`); await db_api.updateRecord('tasks', {key: task_key}, {schedule: schedule}); if (TASKS[task_key]['job']) { TASKS[task_key]['job'].cancel(); TASKS[task_key]['job'] = null; } if (schedule) { TASKS[task_key]['job'] = scheduleJob(task_key, schedule); } } // missing files check async function checkForMissingFiles() { const missing_files = []; const all_files = await db_api.getRecords('files'); for (let i = 0; i < all_files.length; i++) { const file_to_check = all_files[i]; const file_exists = fs.existsSync(file_to_check['path']); if (!file_exists) missing_files.push(file_to_check['uid']); } return {uids: missing_files}; } async function deleteMissingFiles(data) { const uids = data['uids']; for (let i = 0; i < uids.length; i++) { const uid = uids[i]; await db_api.removeRecord('files', {uid: uid}); } } // duplicate files check async function checkForDuplicateFiles() { const duplicate_files = await db_api.findDuplicatesByKey('files', 'path'); const duplicate_uids = duplicate_files.map(duplicate_file => duplicate_file['uid']); if (duplicate_uids && duplicate_uids.length > 0) { return {uids: duplicate_uids}; } return {uids: []}; } async function removeDuplicates(data) { for (let i = 0; i < data['uids'].length; i++) { await db_api.removeRecord('files', {uid: data['uids'][i]}); } } exports.TASKS = TASKS;