Spaces:
Build error
Build error
| const db_api = require('./db'); | |
| const notifications_api = require('./notifications'); | |
| const youtubedl_api = require('./youtube-dl'); | |
| const archive_api = require('./archive'); | |
| const files_api = require('./files'); | |
| const subscriptions_api = require('./subscriptions'); | |
| const config_api = require('./config'); | |
| const auth_api = require('./authentication/auth'); | |
| const utils = require('./utils'); | |
| const logger = require('./logger'); | |
| const CONSTS = require('./consts'); | |
| const fs = require('fs-extra'); | |
| const path = require('path'); | |
| 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: files_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 | |
| }, | |
| delete_old_files: { | |
| run: checkForAutoDeleteFiles, | |
| confirm: autoDeleteFiles, | |
| title: 'Delete old files', | |
| job: null | |
| }, | |
| import_legacy_archives: { | |
| run: archive_api.importArchives, | |
| title: 'Import legacy archives', | |
| job: null | |
| }, | |
| rebuild_database: { | |
| run: rebuildDB, | |
| title: 'Rebuild database', | |
| job: null | |
| } | |
| } | |
| const defaultOptions = { | |
| all: { | |
| auto_confirm: false | |
| }, | |
| delete_old_files: { | |
| blacklist_files: false, | |
| blacklist_subscription_files: false, | |
| threshold_days: '' | |
| } | |
| } | |
| 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, undefined, schedule['data']['tz'] ? schedule['data']['tz'] : undefined); | |
| } 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 mergedDefaultOptions = Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {}); | |
| const task_in_db = await db_api.getRecord('tasks', {key: task_key}); | |
| if (!task_in_db) { | |
| // insert task metadata into table if missing, eventually move title to UI | |
| 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: Object.assign({}, defaultOptions['all'], defaultOptions[task_key] || {}) | |
| }); | |
| } else { | |
| // verify all options exist in task | |
| for (const key of Object.keys(mergedDefaultOptions)) { | |
| const option_key = `options.${key}`; | |
| // Remove any potential mangled option keys (#861) | |
| await db_api.removePropertyFromRecord('tasks', {key: task_key}, {[option_key]: true}); | |
| if (!(task_in_db.options && task_in_db.options.hasOwnProperty(key))) { | |
| await db_api.updateRecord('tasks', {key: task_key}, {[option_key]: mergedDefaultOptions[key]}, true); | |
| } | |
| } | |
| // 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}`); | |
| await db_api.updateRecord('tasks', {key: task_key}, {error: null}) | |
| // 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}`); | |
| const task_obj = await db_api.getRecord('tasks', {key: task_key}); | |
| await notifications_api.sendTaskNotification(task_obj, false); | |
| if (task_obj['options'] && task_obj['options']['auto_confirm']) { | |
| exports.executeConfirm(task_key); | |
| } | |
| } | |
| exports.executeConfirm = async (task_key) => { | |
| logger.verbose(`Confirming task ${task_key}`); | |
| await db_api.updateRecord('tasks', {key: task_key}, {error: null}) | |
| 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}`); | |
| await notifications_api.sendTaskNotification(task_obj, false); | |
| } | |
| 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]}); | |
| } | |
| } | |
| // auto delete files | |
| async function checkForAutoDeleteFiles() { | |
| const task_obj = await db_api.getRecord('tasks', {key: 'delete_old_files'}); | |
| if (!task_obj['options'] || !task_obj['options']['threshold_days']) { | |
| const error_message = 'Failed to do delete check because no limit was set!'; | |
| logger.error(error_message); | |
| await db_api.updateRecord('tasks', {key: 'delete_old_files'}, {error: error_message}) | |
| return null; | |
| } | |
| const delete_older_than_timestamp = Date.now() - task_obj['options']['threshold_days']*86400*1000; | |
| const files = (await db_api.getRecords('files', {registered: {$lt: delete_older_than_timestamp}})) | |
| const files_to_remove = files.map(file => {return {uid: file.uid, sub_id: file.sub_id}}); | |
| return {files_to_remove: files_to_remove}; | |
| } | |
| async function autoDeleteFiles(data) { | |
| const task_obj = await db_api.getRecord('tasks', {key: 'delete_old_files'}); | |
| if (data['files_to_remove']) { | |
| logger.info(`Removing ${data['files_to_remove'].length} old files!`); | |
| for (let i = 0; i < data['files_to_remove'].length; i++) { | |
| const file_to_remove = data['files_to_remove'][i]; | |
| await files_api.deleteFile(file_to_remove['uid'], task_obj['options']['blacklist_files'] || (file_to_remove['sub_id'] && file_to_remove['blacklist_subscription_files'])); | |
| } | |
| } | |
| } | |
| async function rebuildDB() { | |
| await db_api.backupDB(); | |
| let subs_to_add = await guessSubscriptions(false); | |
| subs_to_add = subs_to_add.concat(await guessSubscriptions(true)); | |
| const users_to_add = await guessUsers(); | |
| for (const user_to_add of users_to_add) { | |
| const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); | |
| const user_exists = await db_api.getRecord('users', {uid: user_to_add}); | |
| if (!user_exists) { | |
| await auth_api.registerUser(user_to_add, user_to_add, 'password'); | |
| logger.info(`Regenerated user ${user_to_add}`); | |
| } | |
| const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add); | |
| const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add); | |
| subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs); | |
| } | |
| for (const sub_to_add of subs_to_add) { | |
| const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid'])); | |
| // TODO: we shouldn't be creating this here | |
| const new_sub = Object.assign({}, sub_to_add, {paused: true}); | |
| if (!sub_exists) { | |
| await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true); | |
| logger.info(`Regenerated subscription ${sub_to_add['name']}`); | |
| } | |
| } | |
| logger.info(`Importing unregistered files`); | |
| await files_api.importUnregisteredFiles(); | |
| } | |
| const guessUsers = async () => { | |
| const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); | |
| const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder); | |
| return userPaths.map(userPath => path.basename(userPath)); | |
| } | |
| const guessSubscriptions = async (isPlaylist, basePath = null) => { | |
| const guessed_subs = []; | |
| const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path'); | |
| const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder; | |
| const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels'); | |
| const subs = await utils.getDirectoriesInDirectory(subsPath); | |
| for (const subPath of subs) { | |
| const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH); | |
| if (!fs.existsSync(sub_backup_path)) continue; | |
| try { | |
| const sub_backup = fs.readJSONSync(sub_backup_path) | |
| delete sub_backup['_id']; | |
| guessed_subs.push(sub_backup); | |
| } catch(err) { | |
| logger.warn(`Failed to reimport subscription in path ${subPath}`) | |
| logger.warn(err); | |
| } | |
| } | |
| return guessed_subs; | |
| } | |
| exports.TASKS = TASKS; |