Files
YoutubeDL-Material/backend/db.js
Isaac Abadi b03b4d173b Fixed issue where testing the connecting string would fail if local DB was being used
Fixed issue where blacklisting video in with archiving would not work

Cleaned up unused functions in app.js
2021-08-01 14:38:34 -06:00

1034 lines
36 KiB
JavaScript

var fs = require('fs-extra')
var path = require('path')
var utils = require('./utils')
const { uuid } = require('uuidv4');
const config_api = require('./config');
const { MongoClient } = require("mongodb");
const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync');
const local_adapter = new FileSync('./appdata/local_db.json');
const local_db = low(local_adapter);
var logger = null;
var db = null;
var users_db = null;
var database = null;
const tables = {
files: {
name: 'files',
primary_key: 'uid'
},
playlists: {
name: 'playlists',
primary_key: 'id'
},
categories: {
name: 'categories',
primary_key: 'uid'
},
subscriptions: {
name: 'subscriptions',
primary_key: 'id'
},
downloads: {
name: 'downloads'
},
users: {
name: 'users',
primary_key: 'uid'
},
roles: {
name: 'roles',
primary_key: 'key'
},
test: {
name: 'test'
}
}
const tables_list = Object.keys(tables);
const local_db_defaults = {}
tables_list.forEach(table => {local_db_defaults[table] = []});
local_db.defaults(local_db_defaults).write();
let using_local_db = null;
function setDB(input_db, input_users_db) {
db = input_db; users_db = input_users_db;
exports.db = input_db;
exports.users_db = input_users_db
}
function setLogger(input_logger) {
logger = input_logger;
}
exports.initialize = (input_db, input_users_db, input_logger) => {
setDB(input_db, input_users_db);
setLogger(input_logger);
// must be done here to prevent getConfigItem from being called before init
using_local_db = config_api.getConfigItem('ytdl_use_local_db');
}
exports.connectToDB = async (retries = 5, no_fallback = false, custom_connection_string = null) => {
if (using_local_db && !custom_connection_string) return;
const success = await exports._connectToDB(custom_connection_string);
if (success) return true;
if (retries) {
logger.warn(`MongoDB connection failed! Retrying ${retries} times...`);
const retry_delay_ms = 2000;
for (let i = 0; i < retries; i++) {
const retry_succeeded = await exports._connectToDB();
if (retry_succeeded) {
logger.info(`Successfully connected to DB after ${i+1} attempt(s)`);
return true;
}
if (i !== retries - 1) {
logger.warn(`Retry ${i+1} failed, waiting ${retry_delay_ms}ms before trying again.`);
await utils.wait(retry_delay_ms);
} else {
logger.warn(`Retry ${i+1} failed.`);
}
}
}
if (no_fallback) {
logger.error('Failed to connect to MongoDB. Verify your connection string is valid.');
return;
}
using_local_db = true;
config_api.setConfigItem('ytdl_use_local_db', true);
logger.error('Failed to connect to MongoDB, using Local DB as a fallback. Make sure your MongoDB instance is accessible, or set Local DB as a default through the config.');
return true;
}
exports._connectToDB = async (custom_connection_string = null) => {
const uri = !custom_connection_string ? config_api.getConfigItem('ytdl_mongodb_connection_string') : custom_connection_string; // "mongodb://127.0.0.1:27017/?compressors=zlib&gssapiServiceName=mongodb";
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
try {
await client.connect();
database = client.db('ytdl_material');
// avoid doing anything else if it's just a test
if (custom_connection_string) return true;
const existing_collections = (await database.listCollections({}, { nameOnly: true }).toArray()).map(collection => collection.name);
const missing_tables = tables_list.filter(table => !(existing_collections.includes(table)));
missing_tables.forEach(async table => {
await database.createCollection(table);
});
tables_list.forEach(async table => {
const primary_key = tables[table]['primary_key'];
if (!primary_key) return;
await database.collection(table).createIndex({[primary_key]: 1}, { unique: true });
});
return true;
} catch(err) {
logger.error(err);
return false;
} finally {
// Ensures that the client will close when you finish/error
// await client.close();
}
}
exports.registerFileDB = async (file_path, type, multiUserMode = null, sub = null, customPath = null, category = null, cropFileSettings = null, file_object = null) => {
let db_path = null;
const file_id = utils.removeFileExtension(file_path);
if (!file_object) file_object = generateFileObject(file_id, type, customPath || multiUserMode && multiUserMode.file_path, sub);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
return false;
}
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, customPath || multiUserMode && multiUserMode.file_path);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (multiUserMode) file_object['user_uid'] = multiUserMode.user;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_obj;
}
exports.registerFileDB2 = async (file_path, type, user_uid = null, category = null, sub_id = null, cropFileSettings = null, file_object = null) => {
if (!file_object) file_object = generateFileObject2(file_path, type);
if (!file_object) {
logger.error(`Could not find associated JSON file for ${type} file ${file_path}`);
return false;
}
utils.fixVideoMetadataPerms2(file_path, type);
// add thumbnail path
file_object['thumbnailPath'] = utils.getDownloadedThumbnail2(file_path, type);
// if category exists, only include essential info
if (category) file_object['category'] = {name: category['name'], uid: category['uid']};
// modify duration
if (cropFileSettings) {
file_object['duration'] = (cropFileSettings.cropFileEnd || file_object.duration) - cropFileSettings.cropFileStart;
}
if (user_uid) file_object['user_uid'] = user_uid;
if (sub_id) file_object['sub_id'] = sub_id;
const file_obj = await registerFileDBManual(file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile2(file_path, type)
}
return file_obj;
}
async function registerFileDBManual(file_object) {
// add additional info
file_object['uid'] = uuid();
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
exports.insertRecordIntoTable('files', file_object, {path: file_object['path']})
return file_object;
}
function generateFileObject(id, type, customPath = null, sub = null) {
if (!customPath && sub) {
customPath = getAppendedBasePathSub(sub, config_api.getConfigItem('ytdl_subscriptions_base_path'));
}
var jsonobj = (type === 'audio') ? utils.getJSONMp3(id, customPath, true) : utils.getJSONMp4(id, customPath, true);
if (!jsonobj) {
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const file_path = utils.getTrueFileName(jsonobj['_filename'], type); // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
// console.
var stats = fs.statSync(path.join(__dirname, file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var size = stats.size;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var description = jsonobj.description;
var file_obj = new utils.File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
function generateFileObject2(file_path, type) {
var jsonobj = utils.getJSON(file_path, type);
if (!jsonobj) {
return null;
} else if (!jsonobj['_filename']) {
logger.error(`Failed to get filename from info JSON! File ${jsonobj['title']} could not be added.`);
return null;
}
const ext = (type === 'audio') ? '.mp3' : '.mp4'
const true_file_path = utils.getTrueFileName(jsonobj['_filename'], type);
// console.
var stats = fs.statSync(true_file_path);
const file_id = utils.removeFileExtension(path.basename(file_path));
var title = jsonobj.title;
var url = jsonobj.webpage_url;
var uploader = jsonobj.uploader;
var upload_date = jsonobj.upload_date;
upload_date = upload_date ? `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}` : 'N/A';
var size = stats.size;
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = type === 'audio';
var description = jsonobj.description;
var file_obj = new utils.File(file_id, title, thumbnail, isaudio, duration, url, uploader, size, true_file_path, upload_date, description, jsonobj.view_count, jsonobj.height, jsonobj.abr);
return file_obj;
}
function getAppendedBasePathSub(sub, base_path) {
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
}
exports.getFileDirectoriesAndDBs = async () => {
let dirs_to_check = [];
let subscriptions_to_check = [];
const subscriptions_base_path = config_api.getConfigItem('ytdl_subscriptions_base_path'); // only for single-user mode
const multi_user_mode = config_api.getConfigItem('ytdl_multi_user_mode');
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
const subscriptions_enabled = config_api.getConfigItem('ytdl_allow_subscriptions');
if (multi_user_mode) {
const users = await exports.getRecords('users');
for (let i = 0; i < users.length; i++) {
const user = users[i];
// add user's audio dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'audio'),
user_uid: user.uid,
type: 'audio'
});
// add user's video dir to check list
dirs_to_check.push({
basePath: path.join(usersFileFolder, user.uid, 'video'),
type: 'video'
});
}
} else {
const audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
const videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
// add audio dir to check list
dirs_to_check.push({
basePath: audioFolderPath,
type: 'audio'
});
// add video dir to check list
dirs_to_check.push({
basePath: videoFolderPath,
type: 'video'
});
}
if (subscriptions_enabled) {
const subscriptions = await exports.getRecords('subscriptions');
subscriptions_to_check = subscriptions_to_check.concat(subscriptions);
}
// add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({
basePath: subscription_to_check.user_uid ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),
user_uid: subscription_to_check.user_uid,
type: subscription_to_check.type,
sub_id: subscription_to_check['id']
});
}
return dirs_to_check;
}
exports.importUnregisteredFiles = async () => {
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
// run through check list and check each file to see if it's missing from the db
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type);
for (let j = 0; j < files.length; j++) {
const file = files[j];
// check if file exists in db, if not add it
const file_is_registered = !!(await exports.getRecord('files', {id: file.id, sub_id: dir_to_check.sub_id}))
if (!file_is_registered) {
// add additional info
await exports.registerFileDB2(file['path'], dir_to_check.type, dir_to_check.user_uid, null, dir_to_check.sub_id, null);
logger.verbose(`Added discovered file to the database: ${file.id}`);
}
}
}
}
exports.preimportUnregisteredSubscriptionFile = async (sub, appendedBasePath) => {
const preimported_file_paths = [];
const files = await utils.getDownloadedFilesByType(appendedBasePath, sub.type);
for (let i = 0; i < files.length; i++) {
const file = files[i];
// check if file exists in db, if not add it
const file_is_registered = await exports.getRecord('files', {id: file.id, sub_id: sub.id});
if (!file_is_registered) {
// add additional info
await exports.registerFileDB2(file['path'], sub.type, sub.user_uid, null, sub.id, null, file);
preimported_file_paths.push(file['path']);
logger.verbose(`Preemptively added subscription file to the database: ${file.id}`);
}
}
return preimported_file_paths;
}
exports.addMetadataPropertyToDB = async (property_key) => {
try {
const dirs_to_check = await exports.getFileDirectoriesAndDBs();
const update_obj = {};
for (let i = 0; i < dirs_to_check.length; i++) {
const dir_to_check = dirs_to_check[i];
// recursively get all files in dir's path
const files = await utils.getDownloadedFilesByType(dir_to_check.basePath, dir_to_check.type, true);
for (let j = 0; j < files.length; j++) {
const file = files[j];
if (file[property_key]) {
update_obj[file.uid] = {[property_key]: file[property_key]};
}
}
}
return await exports.bulkUpdateRecords('files', 'uid', update_obj);
} catch(err) {
logger.error(err);
return false;
}
}
exports.createPlaylist = async (playlist_name, uids, type, user_uid = null) => {
const first_video = await exports.getVideo(uids[0]);
const thumbnailToUse = first_video['thumbnailURL'];
let new_playlist = {
name: playlist_name,
uids: uids,
id: uuid(),
thumbnailURL: thumbnailToUse,
type: type,
registered: Date.now(),
randomize_order: false
};
new_playlist.user_uid = user_uid ? user_uid : undefined;
await exports.insertRecordIntoTable('playlists', new_playlist);
const duration = await exports.calculatePlaylistDuration(new_playlist);
await exports.updateRecord('playlists', {id: new_playlist.id}, {duration: duration});
return new_playlist;
}
exports.getPlaylist = async (playlist_id, user_uid = null, require_sharing = false) => {
let playlist = await exports.getRecord('playlists', {id: playlist_id});
if (!playlist) {
playlist = await exports.getRecord('categories', {uid: playlist_id});
if (playlist) {
// category found
const files = await exports.getFiles(user_uid);
utils.addUIDsToCategory(playlist, files);
}
}
// converts playlists to new UID-based schema
if (playlist && playlist['fileNames'] && !playlist['uids']) {
playlist['uids'] = [];
logger.verbose(`Converting playlist ${playlist['name']} to new UID-based schema.`);
for (let i = 0; i < playlist['fileNames'].length; i++) {
const fileName = playlist['fileNames'][i];
const uid = await exports.getVideoUIDByID(fileName, user_uid);
if (uid) playlist['uids'].push(uid);
else logger.warn(`Failed to convert file with name ${fileName} to its UID while converting playlist ${playlist['name']} to the new UID-based schema. The original file is likely missing/deleted and it will be skipped.`);
}
exports.updatePlaylist(playlist, user_uid);
}
// prevent unauthorized users from accessing the file info
if (require_sharing && !playlist['sharingEnabled']) return null;
return playlist;
}
exports.updatePlaylist = async (playlist) => {
let playlistID = playlist.id;
const duration = await exports.calculatePlaylistDuration(playlist);
playlist.duration = duration;
return await exports.updateRecord('playlists', {id: playlistID}, playlist);
}
exports.setPlaylistProperty = async (playlist_id, assignment_obj, user_uid = null) => {
let success = await exports.updateRecord('playlists', {id: playlist_id}, assignment_obj);
if (!success) {
success = await exports.updateRecord('categories', {uid: playlist_id}, assignment_obj);
}
if (!success) {
logger.error(`Could not find playlist or category with ID ${playlist_id}`);
}
return success;
}
exports.calculatePlaylistDuration = async (playlist, playlist_file_objs = null) => {
if (!playlist_file_objs) {
playlist_file_objs = [];
for (let i = 0; i < playlist['uids'].length; i++) {
const uid = playlist['uids'][i];
const file_obj = await exports.getVideo(uid);
if (file_obj) playlist_file_objs.push(file_obj);
}
}
return playlist_file_objs.reduce((a, b) => a + utils.durationStringToNumber(b.duration), 0);
}
exports.deleteFile = async (uid, uuid = null, blacklistMode = false) => {
const file_obj = await exports.getVideo(uid, uuid);
const type = file_obj.isAudio ? 'audio' : 'video';
const folderPath = path.dirname(file_obj.path);
const ext = type === 'audio' ? 'mp3' : 'mp4';
const name = file_obj.id;
const filePathNoExtension = utils.removeFileExtension(file_obj.path);
var jsonPath = `${file_obj.path}.info.json`;
var altJSONPath = `${filePathNoExtension}.info.json`;
var thumbnailPath = `${filePathNoExtension}.webp`;
var altThumbnailPath = `${filePathNoExtension}.jpg`;
jsonPath = path.join(__dirname, jsonPath);
altJSONPath = path.join(__dirname, altJSONPath);
let jsonExists = await fs.pathExists(jsonPath);
let thumbnailExists = await fs.pathExists(thumbnailPath);
if (!jsonExists) {
if (await fs.pathExists(altJSONPath)) {
jsonExists = true;
jsonPath = altJSONPath;
}
}
if (!thumbnailExists) {
if (await fs.pathExists(altThumbnailPath)) {
thumbnailExists = true;
thumbnailPath = altThumbnailPath;
}
}
let fileExists = await fs.pathExists(file_obj.path);
if (config_api.descriptors[uid]) {
try {
for (let i = 0; i < config_api.descriptors[uid].length; i++) {
config_api.descriptors[uid][i].destroy();
}
} catch(e) {
}
}
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
if (useYoutubeDLArchive) {
const archive_path = uuid ? path.join(usersFileFolder, uuid, 'archives', `archive_${type}.txt`) : path.join('appdata', 'archives', `archive_${type}.txt`);
// get ID from JSON
var jsonobj = await (type === 'audio' ? utils.getJSONMp3(name, folderPath) : utils.getJSONMp4(name, folderPath));
let id = null;
if (jsonobj) id = jsonobj.id;
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if (await fs.pathExists(archive_path)) {
const line = id ? await utils.removeIDFromArchive(archive_path, id) : null;
if (blacklistMode && line) await writeToBlacklist(type, line);
} else {
logger.info('Could not find archive file for audio files. Creating...');
await fs.close(await fs.open(archive_path, 'w'));
}
}
if (jsonExists) await fs.unlink(jsonPath);
if (thumbnailExists) await fs.unlink(thumbnailPath);
await exports.removeRecord('files', {uid: uid});
if (fileExists) {
await fs.unlink(file_obj.path);
if (await fs.pathExists(jsonPath) || await fs.pathExists(file_obj.path)) {
return false;
} else {
return true;
}
} else {
// TODO: tell user that the file didn't exist
return true;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports.getVideoUIDByID = async (file_id, uuid = null) => {
const file_obj = await exports.getRecord('files', {id: file_id});
return file_obj ? file_obj['uid'] : null;
}
exports.getVideo = async (file_uid) => {
return await exports.getRecord('files', {uid: file_uid});
}
exports.getFiles = async (uuid = null) => {
return await exports.getRecords('files', {user_uid: uuid});
}
exports.setVideoProperty = async (file_uid, assignment_obj) => {
// TODO: check if video exists, throw error if not
await exports.updateRecord('files', {uid: file_uid}, assignment_obj);
}
// Basic DB functions
// Create
exports.insertRecordIntoTable = async (table, doc, replaceFilter = null) => {
// local db override
if (using_local_db) {
if (replaceFilter) local_db.get(table).remove(replaceFilter).write();
local_db.get(table).push(doc).write();
return true;
}
if (replaceFilter) await database.collection(table).deleteMany(replaceFilter);
const output = await database.collection(table).insertOne(doc);
logger.debug(`Inserted doc into ${table}`);
return !!(output['result']['ok']);
}
exports.insertRecordsIntoTable = async (table, docs, ignore_errors = false) => {
// local db override
if (using_local_db) {
const records_limit = 30000;
if (docs.length < records_limit) {
local_db.get(table).push(...docs).write();
} else {
for (let i = 0; i < docs.length; i+=records_limit) {
const records_to_push = docs.slice(i, i+records_limit > docs.length ? docs.length : i+records_limit)
local_db.get(table).push(...records_to_push).write();
}
}
return true;
}
const output = await database.collection(table).insertMany(docs, {ordered: !ignore_errors});
logger.debug(`Inserted ${output.insertedCount} docs into ${table}`);
return !!(output['result']['ok']);
}
exports.bulkInsertRecordsIntoTable = async (table, docs) => {
// local db override
if (using_local_db) {
return await exports.insertRecordsIntoTable(table, docs);
}
// not a necessary function as insertRecords does the same thing but gives us more control on batch size if needed
const table_collection = database.collection(table);
let bulk = table_collection.initializeOrderedBulkOp(); // Initialize the Ordered Batch
for (let i = 0; i < docs.length; i++) {
bulk.insert(docs[i]);
}
const output = await bulk.execute();
return !!(output['result']['ok']);
}
// Read
exports.getRecord = async (table, filter_obj) => {
// local db override
if (using_local_db) {
return applyFilterLocalDB(local_db.get(table), filter_obj, 'find').value();
}
return await database.collection(table).findOne(filter_obj);
}
exports.getRecords = async (table, filter_obj = null) => {
// local db override
if (using_local_db) {
return filter_obj ? applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').value() : local_db.get(table).value();
}
return filter_obj ? await database.collection(table).find(filter_obj).toArray() : await database.collection(table).find().toArray();
}
// Update
exports.updateRecord = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').assign(update_obj).write();
return true;
}
// sometimes _id will be in the update obj, this breaks mongodb
if (update_obj['_id']) delete update_obj['_id'];
const output = await database.collection(table).updateOne(filter_obj, {$set: update_obj});
return !!(output['result']['ok']);
}
exports.updateRecords = async (table, filter_obj, update_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'filter').assign(update_obj).write();
return true;
}
const output = await database.collection(table).updateMany(filter_obj, {$set: update_obj});
return !!(output['result']['ok']);
}
exports.bulkUpdateRecords = async (table, key_label, update_obj) => {
// local db override
if (using_local_db) {
local_db.get(table).each((record) => {
const item_id_to_update = record[key_label];
if (!update_obj[item_id_to_update]) return;
const props_to_update = Object.keys(update_obj[item_id_to_update]);
for (let i = 0; i < props_to_update.length; i++) {
const prop_to_update = props_to_update[i];
const prop_value = update_obj[item_id_to_update][prop_to_update];
record[prop_to_update] = prop_value;
}
}).write();
return true;
}
const table_collection = database.collection(table);
let bulk = table_collection.initializeOrderedBulkOp(); // Initialize the Ordered Batch
const item_ids_to_update = Object.keys(update_obj);
for (let i = 0; i < item_ids_to_update.length; i++) {
const item_id_to_update = item_ids_to_update[i];
bulk.find({[key_label]: item_id_to_update }).updateOne({
"$set": update_obj[item_id_to_update]
});
}
const output = await bulk.execute();
return !!(output['result']['ok']);
}
exports.pushToRecordsArray = async (table, filter_obj, key, value) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).push(value).write();
return true;
}
const output = await database.collection(table).updateOne(filter_obj, {$push: {[key]: value}});
return !!(output['result']['ok']);
}
exports.pullFromRecordsArray = async (table, filter_obj, key, value) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'find').get(key).pull(value).write();
return true;
}
const output = await database.collection(table).updateOne(filter_obj, {$pull: {[key]: value}});
return !!(output['result']['ok']);
}
// Delete
exports.removeRecord = async (table, filter_obj) => {
// local db override
if (using_local_db) {
applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
return true;
}
const output = await database.collection(table).deleteOne(filter_obj);
return !!(output['result']['ok']);
}
exports.removeAllRecords = async (table = null, filter_obj = null) => {
// local db override
const tables_to_remove = table ? [table] : tables_list;
logger.debug(`Removing all records from: ${tables_to_remove} with filter: ${JSON.stringify(filter_obj)}`)
if (using_local_db) {
for (let i = 0; i < tables_to_remove.length; i++) {
const table_to_remove = tables_to_remove[i];
if (filter_obj) applyFilterLocalDB(local_db.get(table), filter_obj, 'remove').write();
else local_db.assign({[table_to_remove]: []}).write();
logger.debug(`Successfully removed records from ${table_to_remove}`);
}
return true;
}
let success = true;
for (let i = 0; i < tables_to_remove.length; i++) {
const table_to_remove = tables_to_remove[i];
const output = await database.collection(table_to_remove).deleteMany(filter_obj ? filter_obj : {});
logger.debug(`Successfully removed records from ${table_to_remove}`);
success &= !!(output['result']['ok']);
}
return success;
}
// Stats
exports.getDBStats = async () => {
const stats_by_table = {};
for (let i = 0; i < tables_list.length; i++) {
const table = tables_list[i];
if (table === 'test') continue;
stats_by_table[table] = await getDBTableStats(table);
}
return {stats_by_table: stats_by_table, using_local_db: using_local_db};
}
const getDBTableStats = async (table) => {
const table_stats = {};
// local db override
if (using_local_db) {
table_stats['records_count'] = local_db.get(table).value().length;
} else {
const stats = await database.collection(table).stats();
table_stats['records_count'] = stats.count;
}
return table_stats;
}
// JSON to DB
exports.generateJSONTables = async (db_json, users_json) => {
// create records
let files = db_json['files'] || [];
let playlists = db_json['playlists'] || [];
let categories = db_json['categories'] || [];
let subscriptions = db_json['subscriptions'] || [];
const users = users_json['users'];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user['files']) {
user['files'] = user['files'].map(file => ({ ...file, user_uid: user['uid'] }));
files = files.concat(user['files']);
}
if (user['playlists']) {
user['playlists'] = user['playlists'].map(playlist => ({ ...playlist, user_uid: user['uid'] }));
playlists = playlists.concat(user['playlists']);
}
if (user['categories']) {
user['categories'] = user['categories'].map(category => ({ ...category, user_uid: user['uid'] }));
categories = categories.concat(user['categories']);
}
if (user['subscriptions']) {
user['subscriptions'] = user['subscriptions'].map(subscription => ({ ...subscription, user_uid: user['uid'] }));
subscriptions = subscriptions.concat(user['subscriptions']);
}
}
const tables_obj = {};
// TODO: use create*Records funcs to strip unnecessary properties
tables_obj.files = createFilesRecords(files, subscriptions);
tables_obj.playlists = playlists;
tables_obj.categories = categories;
tables_obj.subscriptions = createSubscriptionsRecords(subscriptions);
tables_obj.users = createUsersRecords(users);
tables_obj.roles = createRolesRecords(users_json['roles']);
tables_obj.downloads = createDownloadsRecords(db_json['downloads'])
return tables_obj;
}
exports.importJSONToDB = async (db_json, users_json) => {
await fs.writeFile(`appdata/db.json.${Date.now()/1000}.bak`, JSON.stringify(db_json, null, 2));
await fs.writeFile(`appdata/users_db.json.${Date.now()/1000}.bak`, JSON.stringify(users_json, null, 2));
await exports.removeAllRecords();
const tables_obj = await exports.generateJSONTables(db_json, users_json);
const table_keys = Object.keys(tables_obj);
let success = true;
for (let i = 0; i < table_keys.length; i++) {
const table_key = table_keys[i];
if (!tables_obj[table_key] || tables_obj[table_key].length === 0) continue;
success &= await exports.insertRecordsIntoTable(table_key, tables_obj[table_key], true);
}
return success;
}
const createFilesRecords = (files, subscriptions) => {
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i];
subscription['videos'] = subscription['videos'].map(file => ({ ...file, sub_id: subscription['id'], user_uid: subscription['user_uid'] ? subscription['user_uid'] : undefined}));
files = files.concat(subscriptions[i]['videos']);
}
return files;
}
const createPlaylistsRecords = async (playlists) => {
}
const createCategoriesRecords = async (categories) => {
}
const createSubscriptionsRecords = (subscriptions) => {
for (let i = 0; i < subscriptions.length; i++) {
delete subscriptions[i]['videos'];
}
return subscriptions;
}
const createUsersRecords = (users) => {
users.forEach(user => {
delete user['files'];
delete user['playlists'];
delete user['subscriptions'];
});
return users;
}
const createRolesRecords = (roles) => {
const new_roles = [];
Object.keys(roles).forEach(role_key => {
new_roles.push({
key: role_key,
...roles[role_key]
});
});
return new_roles;
}
const createDownloadsRecords = (downloads) => {
const new_downloads = [];
Object.keys(downloads).forEach(session_key => {
new_downloads.push({
key: session_key,
...downloads[session_key]
});
});
return new_downloads;
}
exports.transferDB = async (local_to_remote) => {
const table_to_records = {};
for (let i = 0; i < tables_list.length; i++) {
const table = tables_list[i];
table_to_records[table] = await exports.getRecords(table);
}
using_local_db = !local_to_remote;
if (local_to_remote) {
// backup local DB
logger.debug('Backup up Local DB...');
await fs.copyFile('appdata/local_db.json', `appdata/local_db.json.${Date.now()/1000}.bak`);
const db_connected = await exports.connectToDB(5, true);
if (!db_connected) {
logger.error('Failed to transfer database - could not connect to MongoDB. Verify that your connection URL is valid.');
return false;
}
}
success = true;
logger.debug('Clearing new database before transfer...');
await exports.removeAllRecords();
logger.debug('Database cleared! Beginning transfer.');
for (let i = 0; i < tables_list.length; i++) {
const table = tables_list[i];
if (!table_to_records[table] || table_to_records[table].length === 0) continue;
success &= await exports.bulkInsertRecordsIntoTable(table, table_to_records[table]);
}
config_api.setConfigItem('ytdl_use_local_db', using_local_db);
logger.debug('Transfer finished!');
return success;
}
/*
This function is necessary to emulate mongodb's ability to search for null or missing values.
A filter of null or undefined for a property will find docs that have that property missing, or have it
null or undefined. We want that same functionality for the local DB as well
*/
const applyFilterLocalDB = (db_path, filter_obj, operation) => {
const filter_props = Object.keys(filter_obj);
const return_val = db_path[operation](record => {
if (!filter_props) return true;
let filtered = true;
for (let i = 0; i < filter_props.length; i++) {
const filter_prop = filter_props[i];
const filter_prop_value = filter_obj[filter_prop];
if (filter_prop_value === undefined || filter_prop_value === null) {
filtered &= record[filter_prop] === undefined || record[filter_prop] === null
} else {
filtered &= record[filter_prop] === filter_prop_value;
}
}
return filtered;
});
return return_val;
}
// archive helper functions
async function writeToBlacklist(type, line) {
const archivePath = path.join(__dirname, 'appdata', 'archives');
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
// adds newline to the beginning of the line
line.replace('\n', '');
line.replace('\r', '');
line = '\n' + line;
await fs.appendFile(blacklistPath, line);
}