mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-10 23:00:57 +03:00
Fixed bug that caused normal archive to be used in multi-user mode Updated login logic when username is not found or user file is missing Fixed bug that prevented playlist sharing from working Added ability to use timestamps when sharing videos
2875 lines
95 KiB
JavaScript
2875 lines
95 KiB
JavaScript
var async = require('async');
|
|
const { uuid } = require('uuidv4');
|
|
var fs = require('fs-extra');
|
|
var auth_api = require('./authentication/auth');
|
|
var winston = require('winston');
|
|
var path = require('path');
|
|
var youtubedl = require('youtube-dl');
|
|
var compression = require('compression');
|
|
var https = require('https');
|
|
var express = require("express");
|
|
var bodyParser = require("body-parser");
|
|
var archiver = require('archiver');
|
|
var unzipper = require('unzipper');
|
|
var mergeFiles = require('merge-files');
|
|
const low = require('lowdb')
|
|
var ProgressBar = require('progress');
|
|
var md5 = require('md5');
|
|
const NodeID3 = require('node-id3')
|
|
const downloader = require('youtube-dl/lib/downloader')
|
|
const fetch = require('node-fetch');
|
|
var URL = require('url').URL;
|
|
const shortid = require('shortid')
|
|
const url_api = require('url');
|
|
var config_api = require('./config.js');
|
|
var subscriptions_api = require('./subscriptions')
|
|
const CONSTS = require('./consts')
|
|
const { spawn } = require('child_process')
|
|
|
|
const is_windows = process.platform === 'win32';
|
|
|
|
var app = express();
|
|
|
|
// database setup
|
|
const FileSync = require('lowdb/adapters/FileSync')
|
|
|
|
const adapter = new FileSync('./appdata/db.json');
|
|
const db = low(adapter)
|
|
|
|
const users_adapter = new FileSync('./appdata/users.json');
|
|
const users_db = low(users_adapter);
|
|
|
|
// check if debug mode
|
|
let debugMode = process.env.YTDL_MODE === 'debug';
|
|
|
|
const admin_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
|
|
|
// logging setup
|
|
|
|
// console format
|
|
const defaultFormat = winston.format.printf(({ level, message, label, timestamp }) => {
|
|
return `${timestamp} ${level.toUpperCase()}: ${message}`;
|
|
});
|
|
const logger = winston.createLogger({
|
|
level: 'info',
|
|
format: winston.format.combine(winston.format.timestamp(), defaultFormat),
|
|
defaultMeta: {},
|
|
transports: [
|
|
//
|
|
// - Write to all logs with level `info` and below to `combined.log`
|
|
// - Write all logs error (and below) to `error.log`.
|
|
//
|
|
new winston.transports.File({ filename: 'appdata/logs/error.log', level: 'error' }),
|
|
new winston.transports.File({ filename: 'appdata/logs/combined.log' }),
|
|
new winston.transports.Console({level: !debugMode ? 'info' : 'debug', name: 'console'})
|
|
]
|
|
});
|
|
|
|
config_api.setLogger(logger);
|
|
subscriptions_api.initialize(db, users_db, logger);
|
|
auth_api.initialize(users_db, logger);
|
|
|
|
// var GithubContent = require('github-content');
|
|
|
|
// Set some defaults
|
|
db.defaults(
|
|
{
|
|
playlists: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
files: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
configWriteFlag: false,
|
|
downloads: {},
|
|
subscriptions: [],
|
|
pin_md5: '',
|
|
files_to_db_migration_complete: false
|
|
}).write();
|
|
|
|
users_db.defaults(
|
|
{
|
|
users: [],
|
|
roles: {
|
|
"admin": {
|
|
"permissions": [
|
|
'filemanager',
|
|
'settings',
|
|
'subscriptions',
|
|
'sharing',
|
|
'advanced_download',
|
|
'downloads_manager'
|
|
]
|
|
}, "user": {
|
|
"permissions": [
|
|
'filemanager',
|
|
'subscriptions',
|
|
'sharing'
|
|
]
|
|
}
|
|
}
|
|
}
|
|
).write();
|
|
|
|
// config values
|
|
var frontendUrl = null;
|
|
var backendUrl = null;
|
|
var backendPort = null;
|
|
var usingEncryption = null;
|
|
var basePath = null;
|
|
var audioFolderPath = null;
|
|
var videoFolderPath = null;
|
|
var downloadOnlyMode = null;
|
|
var useDefaultDownloadingAgent = null;
|
|
var customDownloadingAgent = null;
|
|
var allowSubscriptions = null;
|
|
var subscriptionsCheckInterval = null;
|
|
var archivePath = path.join(__dirname, 'appdata', 'archives');
|
|
|
|
// other needed values
|
|
var options = null; // encryption options
|
|
var url_domain = null;
|
|
var updaterStatus = null;
|
|
|
|
var timestamp_server_start = Date.now();
|
|
|
|
if (debugMode) logger.info('YTDL-Material in debug mode!');
|
|
|
|
// check if just updated
|
|
const just_restarted = fs.existsSync('restart.json');
|
|
if (just_restarted) {
|
|
updaterStatus = {
|
|
updating: false,
|
|
details: 'Update complete! You are now on ' + CONSTS['CURRENT_VERSION']
|
|
}
|
|
fs.unlinkSync('restart.json');
|
|
}
|
|
|
|
// updates & starts youtubedl
|
|
startYoutubeDL();
|
|
|
|
var validDownloadingAgents = [
|
|
'aria2c',
|
|
'avconv',
|
|
'axel',
|
|
'curl',
|
|
'ffmpeg',
|
|
'httpie',
|
|
'wget'
|
|
]
|
|
|
|
// don't overwrite config if it already happened.. NOT
|
|
// let alreadyWritten = db.get('configWriteFlag').value();
|
|
let writeConfigMode = process.env.write_ytdl_config;
|
|
var config = null;
|
|
|
|
// checks if config exists, if not, a config is auto generated
|
|
config_api.configExistsCheck();
|
|
|
|
if (writeConfigMode) {
|
|
setAndLoadConfig();
|
|
} else {
|
|
loadConfig();
|
|
}
|
|
|
|
var downloads = {};
|
|
|
|
app.use(bodyParser.urlencoded({ extended: false }));
|
|
app.use(bodyParser.json());
|
|
|
|
// use passport
|
|
app.use(auth_api.passport.initialize());
|
|
|
|
// objects
|
|
|
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) {
|
|
this.id = id;
|
|
this.title = title;
|
|
this.thumbnailURL = thumbnailURL;
|
|
this.isAudio = isAudio;
|
|
this.duration = duration;
|
|
this.url = url;
|
|
this.uploader = uploader;
|
|
this.size = size;
|
|
this.path = path;
|
|
this.upload_date = upload_date;
|
|
}
|
|
|
|
// actual functions
|
|
|
|
async function checkMigrations() {
|
|
return new Promise(async resolve => {
|
|
// 3.5->3.6 migration
|
|
const files_to_db_migration_complete = db.get('files_to_db_migration_complete').value();
|
|
|
|
if (!files_to_db_migration_complete) {
|
|
logger.info('Beginning migration: 3.5->3.6+')
|
|
runFilesToDBMigration().then(success => {
|
|
if (success) { logger.info('3.5->3.6+ migration complete!'); }
|
|
else { logger.error('Migration failed: 3.5->3.6+'); }
|
|
});
|
|
}
|
|
|
|
resolve(true);
|
|
});
|
|
}
|
|
|
|
async function runFilesToDBMigration() {
|
|
return new Promise(async resolve => {
|
|
try {
|
|
let mp3s = getMp3s();
|
|
let mp4s = getMp4s();
|
|
|
|
for (let i = 0; i < mp3s.length; i++) {
|
|
let file_obj = mp3s[i];
|
|
const file_already_in_db = db.get('files.audio').find({id: file_obj.id}).value();
|
|
if (!file_already_in_db) {
|
|
logger.verbose(`Migrating file ${file_obj.id}`);
|
|
registerFileDB(file_obj.id + '.mp3', 'audio');
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < mp4s.length; i++) {
|
|
let file_obj = mp4s[i];
|
|
const file_already_in_db = db.get('files.video').find({id: file_obj.id}).value();
|
|
if (!file_already_in_db) {
|
|
logger.verbose(`Migrating file ${file_obj.id}`);
|
|
registerFileDB(file_obj.id + '.mp4', 'video');
|
|
}
|
|
}
|
|
|
|
// sets migration to complete
|
|
db.set('files_to_db_migration_complete', true).write();
|
|
resolve(true);
|
|
} catch(err) {
|
|
logger.error(err);
|
|
resolve(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function startServer() {
|
|
if (process.env.USING_HEROKU && process.env.PORT) {
|
|
// default to heroku port if using heroku
|
|
backendPort = process.env.PORT || backendPort;
|
|
|
|
// set config to port
|
|
await setPortItemFromENV();
|
|
}
|
|
if (usingEncryption)
|
|
{
|
|
https.createServer(options, app).listen(backendPort, function() {
|
|
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on port ${backendPort} - using SSL`);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
app.listen(backendPort,function(){
|
|
logger.info(`YoutubeDL-Material ${CONSTS['CURRENT_VERSION']} started on PORT ${backendPort}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function restartServer() {
|
|
const restartProcess = () => {
|
|
spawn('node', ['app.js'], {
|
|
detached: true,
|
|
stdio: 'inherit'
|
|
}).unref()
|
|
process.exit()
|
|
}
|
|
logger.info('Update complete! Restarting server...');
|
|
|
|
// the following line restarts the server through nodemon
|
|
fs.writeFileSync('restart.json', 'internal use only');
|
|
}
|
|
|
|
async function updateServer(tag) {
|
|
// no tag provided means update to the latest version
|
|
if (!tag) {
|
|
const new_version_available = await isNewVersionAvailable();
|
|
if (!new_version_available) {
|
|
logger.error('ERROR: Failed to update - no update is available.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return new Promise(async resolve => {
|
|
// backup current dir
|
|
updaterStatus = {
|
|
updating: true,
|
|
'details': 'Backing up key server files...'
|
|
}
|
|
let backup_succeeded = await backupServerLite();
|
|
if (!backup_succeeded) {
|
|
resolve(false);
|
|
return false;
|
|
}
|
|
|
|
updaterStatus = {
|
|
updating: true,
|
|
'details': 'Downloading requested release...'
|
|
}
|
|
// grab new package.json and public folder
|
|
await downloadReleaseFiles(tag);
|
|
|
|
updaterStatus = {
|
|
updating: true,
|
|
'details': 'Installing new dependencies...'
|
|
}
|
|
// run npm install
|
|
await installDependencies();
|
|
|
|
updaterStatus = {
|
|
updating: true,
|
|
'details': 'Update complete! Restarting server...'
|
|
}
|
|
restartServer();
|
|
}, err => {
|
|
updaterStatus = {
|
|
updating: false,
|
|
error: true,
|
|
'details': 'Update failed. Check error logs for more info.'
|
|
}
|
|
});
|
|
}
|
|
|
|
async function downloadReleaseFiles(tag) {
|
|
tag = tag ? tag : await getLatestVersion();
|
|
return new Promise(async resolve => {
|
|
logger.info('Downloading new files...')
|
|
|
|
// downloads the latest release zip file
|
|
await downloadReleaseZip(tag);
|
|
|
|
// deletes contents of public dir
|
|
fs.removeSync(path.join(__dirname, 'public'));
|
|
fs.mkdirSync(path.join(__dirname, 'public'));
|
|
|
|
let replace_ignore_list = ['youtubedl-material/appdata/default.json',
|
|
'youtubedl-material/appdata/db.json',
|
|
'youtubedl-material/appdata/users.json',
|
|
'youtubedl-material/appdata/*']
|
|
logger.info(`Installing update ${tag}...`)
|
|
|
|
// downloads new package.json and adds new public dir files from the downloaded zip
|
|
fs.createReadStream(path.join(__dirname, `youtubedl-material-release-${tag}.zip`)).pipe(unzipper.Parse())
|
|
.on('entry', function (entry) {
|
|
var fileName = entry.path;
|
|
var type = entry.type; // 'Directory' or 'File'
|
|
var size = entry.size;
|
|
var is_dir = fileName.substring(fileName.length-1, fileName.length) === '/'
|
|
if (!is_dir && fileName.includes('youtubedl-material/public/')) {
|
|
// get public folder files
|
|
var actualFileName = fileName.replace('youtubedl-material/public/', '');
|
|
if (actualFileName.length !== 0 && actualFileName.substring(actualFileName.length-1, actualFileName.length) !== '/') {
|
|
fs.ensureDirSync(path.join(__dirname, 'public', path.dirname(actualFileName)));
|
|
entry.pipe(fs.createWriteStream(path.join(__dirname, 'public', actualFileName)));
|
|
} else {
|
|
entry.autodrain();
|
|
}
|
|
} else if (!is_dir && !replace_ignore_list.includes(fileName)) {
|
|
// get package.json
|
|
var actualFileName = fileName.replace('youtubedl-material/', '');
|
|
logger.verbose('Downloading file ' + actualFileName);
|
|
entry.pipe(fs.createWriteStream(path.join(__dirname, actualFileName)));
|
|
} else {
|
|
entry.autodrain();
|
|
}
|
|
})
|
|
.on('close', function () {
|
|
resolve(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
// helper function to download file using fetch
|
|
async function fetchFile(url, path, file_label) {
|
|
var len = null;
|
|
const res = await fetch(url);
|
|
|
|
len = parseInt(res.headers.get("Content-Length"), 10);
|
|
|
|
var bar = new ProgressBar(` Downloading ${file_label} [:bar] :percent :etas`, {
|
|
complete: '=',
|
|
incomplete: ' ',
|
|
width: 20,
|
|
total: len
|
|
});
|
|
const fileStream = fs.createWriteStream(path);
|
|
await new Promise((resolve, reject) => {
|
|
res.body.pipe(fileStream);
|
|
res.body.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
res.body.on('data', function (chunk) {
|
|
bar.tick(chunk.length);
|
|
});
|
|
fileStream.on("finish", function() {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function downloadReleaseZip(tag) {
|
|
return new Promise(async resolve => {
|
|
// get name of zip file, which depends on the version
|
|
const latest_release_link = `https://github.com/Tzahi12345/YoutubeDL-Material/releases/download/${tag}/`;
|
|
const tag_without_v = tag.substring(1, tag.length);
|
|
const zip_file_name = `youtubedl-material-${tag_without_v}.zip`
|
|
const latest_zip_link = latest_release_link + zip_file_name;
|
|
let output_path = path.join(__dirname, `youtubedl-material-release-${tag}.zip`);
|
|
|
|
// download zip from release
|
|
await fetchFile(latest_zip_link, output_path, 'update ' + tag);
|
|
resolve(true);
|
|
});
|
|
|
|
}
|
|
|
|
async function installDependencies() {
|
|
return new Promise(resolve => {
|
|
var child_process = require('child_process');
|
|
child_process.execSync('npm install',{stdio:[0,1,2]});
|
|
resolve(true);
|
|
});
|
|
|
|
}
|
|
|
|
async function backupServerLite() {
|
|
return new Promise(async resolve => {
|
|
fs.ensureDirSync(path.join(__dirname, 'appdata', 'backups'));
|
|
let output_path = path.join('appdata', 'backups', `backup-${Date.now()}.zip`);
|
|
logger.info(`Backing up your non-video/audio files to ${output_path}. This may take up to a few seconds/minutes.`);
|
|
let output = fs.createWriteStream(path.join(__dirname, output_path));
|
|
var archive = archiver('zip', {
|
|
gzip: true,
|
|
zlib: { level: 9 } // Sets the compression level.
|
|
});
|
|
|
|
archive.on('error', function(err) {
|
|
logger.error(err);
|
|
resolve(false);
|
|
});
|
|
|
|
// pipe archive data to the output file
|
|
archive.pipe(output);
|
|
|
|
// ignore certain directories (ones with video or audio files)
|
|
const files_to_ignore = [path.join(config_api.getConfigItem('ytdl_subscriptions_base_path'), '**'),
|
|
path.join(config_api.getConfigItem('ytdl_audio_folder_path'), '**'),
|
|
path.join(config_api.getConfigItem('ytdl_video_folder_path'), '**'),
|
|
'appdata/backups/backup-*.zip'];
|
|
|
|
archive.glob('**/*', {
|
|
ignore: files_to_ignore
|
|
});
|
|
|
|
await archive.finalize();
|
|
|
|
// wait a tiny bit for the zip to reload in fs
|
|
setTimeout(function() {
|
|
resolve(true);
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
async function isNewVersionAvailable() {
|
|
return new Promise(async resolve => {
|
|
// gets tag of the latest version of youtubedl-material, compare to current version
|
|
const latest_tag = await getLatestVersion();
|
|
const current_tag = CONSTS['CURRENT_VERSION'];
|
|
if (latest_tag > current_tag) {
|
|
resolve(true);
|
|
} else {
|
|
resolve(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function getLatestVersion() {
|
|
return new Promise(resolve => {
|
|
fetch('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest', {method: 'Get'})
|
|
.then(async res => res.json())
|
|
.then(async (json) => {
|
|
if (json['message']) {
|
|
// means there's an error in getting latest version
|
|
logger.error(`ERROR: Received the following message from GitHub's API:`);
|
|
logger.error(json['message']);
|
|
if (json['documentation_url']) logger.error(`Associated URL: ${json['documentation_url']}`)
|
|
}
|
|
resolve(json['tag_name']);
|
|
return;
|
|
});
|
|
});
|
|
}
|
|
|
|
async function setPortItemFromENV() {
|
|
return new Promise(resolve => {
|
|
config_api.setConfigItem('ytdl_port', backendPort.toString());
|
|
setTimeout(() => resolve(true), 100);
|
|
});
|
|
}
|
|
|
|
async function setAndLoadConfig() {
|
|
await setConfigFromEnv();
|
|
await loadConfig();
|
|
}
|
|
|
|
async function setConfigFromEnv() {
|
|
return new Promise(resolve => {
|
|
let config_items = getEnvConfigItems();
|
|
let success = config_api.setConfigItems(config_items);
|
|
if (success) {
|
|
logger.info('Config items set using ENV variables.');
|
|
setTimeout(() => resolve(true), 100);
|
|
} else {
|
|
logger.error('ERROR: Failed to set config items using ENV variables.');
|
|
resolve(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadConfig() {
|
|
return new Promise(async resolve => {
|
|
loadConfigValues();
|
|
|
|
// creates archive path if missing
|
|
if (!fs.existsSync(archivePath)){
|
|
fs.mkdirSync(archivePath);
|
|
}
|
|
|
|
// get subscriptions
|
|
if (allowSubscriptions) {
|
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
|
watchSubscriptions();
|
|
setInterval(() => {
|
|
watchSubscriptions();
|
|
}, subscriptionsCheckInterval * 1000);
|
|
}
|
|
|
|
// check migrations
|
|
await checkMigrations();
|
|
|
|
// load in previous downloads
|
|
downloads = db.get('downloads').value();
|
|
|
|
// start the server here
|
|
startServer();
|
|
|
|
resolve(true);
|
|
});
|
|
|
|
}
|
|
|
|
function loadConfigValues() {
|
|
url = !debugMode ? config_api.getConfigItem('ytdl_url') : 'http://localhost:4200';
|
|
backendPort = config_api.getConfigItem('ytdl_port');
|
|
usingEncryption = config_api.getConfigItem('ytdl_use_encryption');
|
|
audioFolderPath = config_api.getConfigItem('ytdl_audio_folder_path');
|
|
videoFolderPath = config_api.getConfigItem('ytdl_video_folder_path');
|
|
downloadOnlyMode = config_api.getConfigItem('ytdl_download_only_mode');
|
|
useDefaultDownloadingAgent = config_api.getConfigItem('ytdl_use_default_downloading_agent');
|
|
customDownloadingAgent = config_api.getConfigItem('ytdl_custom_downloading_agent');
|
|
allowSubscriptions = config_api.getConfigItem('ytdl_allow_subscriptions');
|
|
subscriptionsCheckInterval = config_api.getConfigItem('ytdl_subscriptions_check_interval');
|
|
|
|
if (!useDefaultDownloadingAgent && validDownloadingAgents.indexOf(customDownloadingAgent) !== -1 ) {
|
|
logger.info(`Using non-default downloading agent \'${customDownloadingAgent}\'`)
|
|
} else {
|
|
customDownloadingAgent = null;
|
|
}
|
|
|
|
if (usingEncryption)
|
|
{
|
|
var certFilePath = path.resolve(config_api.getConfigItem('ytdl_cert_file_path'));
|
|
var keyFilePath = path.resolve(config_api.getConfigItem('ytdl_key_file_path'));
|
|
|
|
var certKeyFile = fs.readFileSync(keyFilePath);
|
|
var certFile = fs.readFileSync(certFilePath);
|
|
|
|
options = {
|
|
key: certKeyFile,
|
|
cert: certFile
|
|
};
|
|
}
|
|
|
|
url_domain = new URL(url);
|
|
|
|
let logger_level = config_api.getConfigItem('ytdl_logger_level');
|
|
const possible_levels = ['error', 'warn', 'info', 'verbose', 'debug'];
|
|
if (!possible_levels.includes(logger_level)) {
|
|
logger.error(`${logger_level} is not a valid logger level! Choose one of the following: ${possible_levels.join(', ')}.`)
|
|
logger_level = 'info';
|
|
}
|
|
logger.level = logger_level;
|
|
winston.loggers.get('console').level = logger_level;
|
|
logger.transports[2].level = logger_level;
|
|
}
|
|
|
|
function calculateSubcriptionRetrievalDelay(amount) {
|
|
// frequency is 5 mins
|
|
let frequency_in_ms = subscriptionsCheckInterval * 1000;
|
|
let minimum_frequency = 60 * 1000;
|
|
const first_frequency = frequency_in_ms/amount;
|
|
return (first_frequency < minimum_frequency) ? minimum_frequency : first_frequency;
|
|
}
|
|
|
|
function watchSubscriptions() {
|
|
let subscriptions = null;
|
|
|
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
|
if (multiUserMode) {
|
|
subscriptions = [];
|
|
let users = users_db.get('users').value();
|
|
for (let i = 0; i < users.length; i++) {
|
|
if (users[i]['subscriptions']) subscriptions = subscriptions.concat(users[i]['subscriptions']);
|
|
}
|
|
} else {
|
|
subscriptions = subscriptions_api.getAllSubscriptions();
|
|
}
|
|
|
|
if (!subscriptions) return;
|
|
|
|
let subscriptions_amount = subscriptions.length;
|
|
let delay_interval = calculateSubcriptionRetrievalDelay(subscriptions_amount);
|
|
|
|
let current_delay = 0;
|
|
for (let i = 0; i < subscriptions.length; i++) {
|
|
let sub = subscriptions[i];
|
|
logger.verbose('Watching ' + sub.name + ' with delay interval of ' + delay_interval);
|
|
setTimeout(() => {
|
|
subscriptions_api.getVideosForSub(sub, sub.user_uid);
|
|
}, current_delay);
|
|
current_delay += delay_interval;
|
|
if (current_delay >= subscriptionsCheckInterval * 1000) current_delay = 0;
|
|
}
|
|
}
|
|
|
|
function getOrigin() {
|
|
return url_domain.origin;
|
|
}
|
|
|
|
// gets a list of config items that are stored as an environment variable
|
|
function getEnvConfigItems() {
|
|
let config_items = [];
|
|
|
|
let config_item_keys = Object.keys(config_api.CONFIG_ITEMS);
|
|
for (let i = 0; i < config_item_keys.length; i++) {
|
|
let key = config_item_keys[i];
|
|
if (process['env'][key]) {
|
|
const config_item = generateEnvVarConfigItem(key);
|
|
config_items.push(config_item);
|
|
}
|
|
}
|
|
|
|
return config_items;
|
|
}
|
|
|
|
// gets value of a config item and stores it in an object
|
|
function generateEnvVarConfigItem(key) {
|
|
return {key: key, value: process['env'][key]};
|
|
}
|
|
|
|
function getMp3s() {
|
|
let mp3s = [];
|
|
var files = recFindByExt(audioFolderPath, 'mp3'); // fs.readdirSync(audioFolderPath);
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i];
|
|
var file_path = file.substring(audioFolderPath.length, file.length);
|
|
|
|
var stats = fs.statSync(file);
|
|
|
|
var id = file_path.substring(0, file_path.length-4);
|
|
var jsonobj = getJSONMp3(id);
|
|
if (!jsonobj) continue;
|
|
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)}` : null;
|
|
|
|
var size = stats.size;
|
|
|
|
var thumbnail = jsonobj.thumbnail;
|
|
var duration = jsonobj.duration;
|
|
var isaudio = true;
|
|
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
|
mp3s.push(file_obj);
|
|
}
|
|
return mp3s;
|
|
}
|
|
|
|
function getMp4s(relative_path = true) {
|
|
let mp4s = [];
|
|
var files = recFindByExt(videoFolderPath, 'mp4');
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i];
|
|
var file_path = file.substring(videoFolderPath.length, file.length);
|
|
|
|
var stats = fs.statSync(file);
|
|
|
|
var id = file_path.substring(0, file_path.length-4);
|
|
var jsonobj = getJSONMp4(id);
|
|
if (!jsonobj) continue;
|
|
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)}` : null;
|
|
var thumbnail = jsonobj.thumbnail;
|
|
var duration = jsonobj.duration;
|
|
|
|
var size = stats.size;
|
|
|
|
var isaudio = false;
|
|
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
|
mp4s.push(file_obj);
|
|
}
|
|
return mp4s;
|
|
}
|
|
|
|
function getThumbnailMp3(name)
|
|
{
|
|
var obj = getJSONMp3(name);
|
|
var thumbnailLink = obj.thumbnail;
|
|
return thumbnailLink;
|
|
}
|
|
|
|
function getThumbnailMp4(name)
|
|
{
|
|
var obj = getJSONMp4(name);
|
|
var thumbnailLink = obj.thumbnail;
|
|
return thumbnailLink;
|
|
}
|
|
|
|
function getFileSizeMp3(name)
|
|
{
|
|
var jsonPath = audioFolderPath+name+".mp3.info.json";
|
|
|
|
if (fs.existsSync(jsonPath))
|
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
else
|
|
var obj = 0;
|
|
|
|
return obj.filesize;
|
|
}
|
|
|
|
function getFileSizeMp4(name)
|
|
{
|
|
var jsonPath = videoFolderPath+name+".info.json";
|
|
var filesize = 0;
|
|
if (fs.existsSync(jsonPath))
|
|
{
|
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
var format = obj.format.substring(0,3);
|
|
for (i = 0; i < obj.formats.length; i++)
|
|
{
|
|
if (obj.formats[i].format_id == format)
|
|
{
|
|
filesize = obj.formats[i].filesize;
|
|
}
|
|
}
|
|
}
|
|
|
|
return filesize;
|
|
}
|
|
|
|
function getJSONMp3(name, customPath = null, openReadPerms = false)
|
|
{
|
|
var jsonPath = audioFolderPath+name+".info.json";
|
|
var alternateJsonPath = audioFolderPath+name+".mp3.info.json";
|
|
if (!customPath) {
|
|
jsonPath = audioFolderPath + name + ".info.json";
|
|
} else {
|
|
jsonPath = customPath + name + ".info.json";
|
|
alternateJsonPath = customPath + name + ".mp3.info.json";
|
|
}
|
|
var obj = null;
|
|
if (fs.existsSync(jsonPath)) {
|
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
if (!is_windows && openReadPerms) fs.chmodSync(jsonPath, 0o755);
|
|
}
|
|
else if (fs.existsSync(alternateJsonPath)) {
|
|
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
|
if (!is_windows && openReadPerms) fs.chmodSync(alternateJsonPath, 0o755);
|
|
}
|
|
else
|
|
obj = 0;
|
|
|
|
return obj;
|
|
}
|
|
|
|
function getJSONMp4(name, customPath = null, openReadPerms = false)
|
|
{
|
|
var obj = null; // output
|
|
let jsonPath = null;
|
|
var alternateJsonPath = videoFolderPath + name + ".mp4.info.json";
|
|
if (!customPath) {
|
|
jsonPath = videoFolderPath + name + ".info.json";
|
|
} else {
|
|
jsonPath = customPath + name + ".info.json";
|
|
alternateJsonPath = customPath + name + ".mp4.info.json";
|
|
}
|
|
if (fs.existsSync(jsonPath))
|
|
{
|
|
obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
if (openReadPerms) fs.chmodSync(jsonPath, 0o644);
|
|
} else if (fs.existsSync(alternateJsonPath)) {
|
|
obj = JSON.parse(fs.readFileSync(alternateJsonPath, 'utf8'));
|
|
if (openReadPerms) fs.chmodSync(alternateJsonPath, 0o644);
|
|
}
|
|
else obj = 0;
|
|
return obj;
|
|
}
|
|
|
|
function getAmountDownloadedMp3(name)
|
|
{
|
|
var partPath = audioFolderPath+name+".mp3.part";
|
|
if (fs.existsSync(partPath))
|
|
{
|
|
const stats = fs.statSync(partPath);
|
|
const fileSizeInBytes = stats.size;
|
|
return fileSizeInBytes;
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
function getAmountDownloadedMp4(name)
|
|
{
|
|
var format = getVideoFormatID(name);
|
|
var partPath = videoFolderPath+name+".f"+format+".mp4.part";
|
|
if (fs.existsSync(partPath))
|
|
{
|
|
const stats = fs.statSync(partPath);
|
|
const fileSizeInBytes = stats.size;
|
|
return fileSizeInBytes;
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
function getVideoFormatID(name)
|
|
{
|
|
var jsonPath = videoFolderPath+name+".info.json";
|
|
if (fs.existsSync(jsonPath))
|
|
{
|
|
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
var format = obj.format.substring(0,3);
|
|
return format;
|
|
}
|
|
}
|
|
|
|
async function createPlaylistZipFile(fileNames, type, outputName, fullPathProvided = null) {
|
|
return new Promise(async resolve => {
|
|
let zipFolderPath = null;
|
|
|
|
if (!fullPathProvided) {
|
|
zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
|
|
} else {
|
|
zipFolderPath = path.join(__dirname, config_api.getConfigItem('ytdl_subscriptions_base_path'));
|
|
}
|
|
|
|
let ext = (type === 'audio') ? '.mp3' : '.mp4';
|
|
|
|
let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip'));
|
|
|
|
var archive = archiver('zip', {
|
|
gzip: true,
|
|
zlib: { level: 9 } // Sets the compression level.
|
|
});
|
|
|
|
archive.on('error', function(err) {
|
|
logger.error(err);
|
|
throw err;
|
|
});
|
|
|
|
// pipe archive data to the output file
|
|
archive.pipe(output);
|
|
|
|
for (let i = 0; i < fileNames.length; i++) {
|
|
let fileName = fileNames[i];
|
|
let fileNamePathRemoved = path.parse(fileName).base;
|
|
let file_path = !fullPathProvided ? zipFolderPath + fileName + ext : fileName;
|
|
archive.file(file_path, {name: fileNamePathRemoved + ext})
|
|
}
|
|
|
|
await archive.finalize();
|
|
|
|
// wait a tiny bit for the zip to reload in fs
|
|
setTimeout(function() {
|
|
resolve(path.join(zipFolderPath,outputName + '.zip'));
|
|
}, 100);
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
async function deleteAudioFile(name, blacklistMode = false) {
|
|
return new Promise(resolve => {
|
|
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
|
|
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
|
|
var altJSONPath = path.join(audioFolderPath,name+'.info.json');
|
|
var audioFilePath = path.join(audioFolderPath,name+'.mp3');
|
|
jsonPath = path.join(__dirname, jsonPath);
|
|
altJSONPath = path.join(__dirname, altJSONPath);
|
|
audioFilePath = path.join(__dirname, audioFilePath);
|
|
|
|
let jsonExists = fs.existsSync(jsonPath);
|
|
|
|
if (!jsonExists) {
|
|
if (fs.existsSync(altJSONPath)) {
|
|
jsonExists = true;
|
|
jsonPath = altJSONPath;
|
|
}
|
|
}
|
|
|
|
let audioFileExists = fs.existsSync(audioFilePath);
|
|
|
|
if (config_api.descriptors[name]) {
|
|
try {
|
|
for (let i = 0; i < config_api.descriptors[name].length; i++) {
|
|
config_api.descriptors[name][i].destroy();
|
|
}
|
|
} catch(e) {
|
|
|
|
}
|
|
}
|
|
|
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
if (useYoutubeDLArchive) {
|
|
const archive_path = path.join(archivePath, 'archive_audio.txt');
|
|
|
|
// get ID from JSON
|
|
|
|
var jsonobj = getJSONMp3(name);
|
|
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 (fs.existsSync(archive_path)) {
|
|
const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
|
if (blacklistMode && line) writeToBlacklist('audio', line);
|
|
} else {
|
|
logger.info('Could not find archive file for audio files. Creating...');
|
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
|
}
|
|
}
|
|
|
|
if (jsonExists) fs.unlinkSync(jsonPath);
|
|
if (audioFileExists) {
|
|
fs.unlink(audioFilePath, function(err) {
|
|
if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) {
|
|
resolve(false);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
} else {
|
|
// TODO: tell user that the file didn't exist
|
|
resolve(true);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
async function deleteVideoFile(name, customPath = null, blacklistMode = false) {
|
|
return new Promise(resolve => {
|
|
let filePath = customPath ? customPath : videoFolderPath;
|
|
var jsonPath = path.join(filePath,name+'.info.json');
|
|
var videoFilePath = path.join(filePath,name+'.mp4');
|
|
jsonPath = path.join(__dirname, jsonPath);
|
|
videoFilePath = path.join(__dirname, videoFilePath);
|
|
|
|
jsonExists = fs.existsSync(jsonPath);
|
|
videoFileExists = fs.existsSync(videoFilePath);
|
|
|
|
if (config_api.descriptors[name]) {
|
|
try {
|
|
for (let i = 0; i < config_api.descriptors[name].length; i++) {
|
|
config_api.descriptors[name][i].destroy();
|
|
}
|
|
} catch(e) {
|
|
|
|
}
|
|
}
|
|
|
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
if (useYoutubeDLArchive) {
|
|
const archive_path = path.join(archivePath, 'archive_video.txt');
|
|
|
|
// get ID from JSON
|
|
|
|
var jsonobj = getJSONMp4(name);
|
|
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 (fs.existsSync(archive_path)) {
|
|
const line = id ? subscriptions_api.removeIDFromArchive(archive_path, id) : null;
|
|
if (blacklistMode && line) writeToBlacklist('video', line);
|
|
} else {
|
|
logger.info('Could not find archive file for videos. Creating...');
|
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
|
}
|
|
}
|
|
|
|
if (jsonExists) fs.unlinkSync(jsonPath);
|
|
if (videoFileExists) {
|
|
fs.unlink(videoFilePath, function(err) {
|
|
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
|
|
resolve(false);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
} else {
|
|
// TODO: tell user that the file didn't exist
|
|
resolve(true);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
function recFindByExt(base,ext,files,result)
|
|
{
|
|
files = files || fs.readdirSync(base)
|
|
result = result || []
|
|
|
|
files.forEach(
|
|
function (file) {
|
|
var newbase = path.join(base,file)
|
|
if ( fs.statSync(newbase).isDirectory() )
|
|
{
|
|
result = recFindByExt(newbase,ext,fs.readdirSync(newbase),result)
|
|
}
|
|
else
|
|
{
|
|
if ( file.substr(-1*(ext.length+1)) == '.' + ext )
|
|
{
|
|
result.push(newbase)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
return result
|
|
}
|
|
|
|
function registerFileDB(file_path, type, multiUserMode = null) {
|
|
const file_id = file_path.substring(0, file_path.length-4);
|
|
const file_object = generateFileObject(file_id, type, multiUserMode && multiUserMode.file_path);
|
|
if (!file_object) {
|
|
logger.error(`Could not find associated JSON file for ${type} file ${file_id}`);
|
|
return false;
|
|
}
|
|
|
|
// 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);
|
|
|
|
if (multiUserMode) {
|
|
auth_api.registerUserFile(multiUserMode.user, file_object, type);
|
|
} else {
|
|
// remove existing video if overwriting
|
|
db.get(`files.${type}`)
|
|
.remove({
|
|
path: file_object['path']
|
|
}).write();
|
|
|
|
db.get(`files.${type}`)
|
|
.push(file_object)
|
|
.write();
|
|
}
|
|
|
|
return file_object['uid'];
|
|
}
|
|
|
|
function generateFileObject(id, type, customPath = null) {
|
|
var jsonobj = (type === 'audio') ? getJSONMp3(id, customPath, true) : getJSONMp4(id, customPath, true);
|
|
if (!jsonobj) {
|
|
return null;
|
|
}
|
|
const ext = (type === 'audio') ? '.mp3' : '.mp4'
|
|
const file_path = 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 file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file_path, upload_date);
|
|
return file_obj;
|
|
}
|
|
|
|
// replaces .webm with appropriate extension
|
|
function getTrueFileName(unfixed_path, type) {
|
|
let fixed_path = unfixed_path;
|
|
|
|
const new_ext = (type === 'audio' ? 'mp3' : 'mp4');
|
|
let unfixed_parts = unfixed_path.split('.');
|
|
const old_ext = unfixed_parts[unfixed_parts.length-1];
|
|
|
|
|
|
if (old_ext !== new_ext) {
|
|
unfixed_parts[unfixed_parts.length-1] = new_ext;
|
|
fixed_path = unfixed_parts.join('.');
|
|
}
|
|
return fixed_path;
|
|
}
|
|
|
|
function getAudioInfos(fileNames) {
|
|
let result = [];
|
|
for (let i = 0; i < fileNames.length; i++) {
|
|
let fileName = fileNames[i];
|
|
let fileLocation = audioFolderPath+fileName+'.mp3.info.json';
|
|
if (fs.existsSync(fileLocation)) {
|
|
let data = fs.readFileSync(fileLocation);
|
|
try {
|
|
result.push(JSON.parse(data));
|
|
} catch(e) {
|
|
logger.error(`Could not find info for file ${fileName}.mp3`);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getVideoInfos(fileNames) {
|
|
let result = [];
|
|
for (let i = 0; i < fileNames.length; i++) {
|
|
let fileName = fileNames[i];
|
|
let fileLocation = videoFolderPath+fileName+'.info.json';
|
|
if (fs.existsSync(fileLocation)) {
|
|
let data = fs.readFileSync(fileLocation);
|
|
try {
|
|
result.push(JSON.parse(data));
|
|
} catch(e) {
|
|
logger.error(`Could not find info for file ${fileName}.mp4`);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// downloads
|
|
|
|
async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
|
return new Promise(async resolve => {
|
|
var date = Date.now();
|
|
|
|
// audio / video specific vars
|
|
var is_audio = type === 'audio';
|
|
var ext = is_audio ? '.mp3' : '.mp4';
|
|
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
|
|
|
// prepend with user if needed
|
|
let multiUserMode = null;
|
|
if (options.user) {
|
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
const user_path = path.join(usersFileFolder, options.user, type);
|
|
fs.ensureDirSync(user_path);
|
|
fileFolderPath = user_path + path.sep;
|
|
multiUserMode = {
|
|
user: options.user,
|
|
file_path: fileFolderPath
|
|
}
|
|
options.customFileFolderPath = fileFolderPath;
|
|
}
|
|
|
|
const downloadConfig = await generateArgs(url, type, options);
|
|
|
|
// adds download to download helper
|
|
const download_uid = uuid();
|
|
const session = sessionID ? sessionID : 'undeclared';
|
|
if (!downloads[session]) downloads[session] = {};
|
|
downloads[session][download_uid] = {
|
|
uid: download_uid,
|
|
ui_uid: options.ui_uid,
|
|
downloading: true,
|
|
complete: false,
|
|
url: url,
|
|
type: type,
|
|
percent_complete: 0,
|
|
is_playlist: url.includes('playlist'),
|
|
timestamp_start: Date.now()
|
|
};
|
|
const download = downloads[session][download_uid];
|
|
updateDownloads();
|
|
|
|
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
|
|
download['downloading'] = false;
|
|
download['timestamp_end'] = Date.now();
|
|
var file_uid = null;
|
|
let new_date = Date.now();
|
|
let difference = (new_date - date)/1000;
|
|
logger.debug(`${is_audio ? 'Audio' : 'Video'} download delay: ${difference} seconds.`);
|
|
if (err) {
|
|
logger.error(err.stderr);
|
|
|
|
download['error'] = err.stderr;
|
|
updateDownloads();
|
|
resolve(false);
|
|
throw err;
|
|
} else if (output) {
|
|
if (output.length === 0 || output[0].length === 0) {
|
|
download['error'] = 'No output. Check if video already exists in your archive.';
|
|
updateDownloads();
|
|
|
|
resolve(false);
|
|
return;
|
|
}
|
|
var file_names = [];
|
|
for (let i = 0; i < output.length; i++) {
|
|
let output_json = null;
|
|
try {
|
|
output_json = JSON.parse(output[i]);
|
|
} catch(e) {
|
|
output_json = null;
|
|
}
|
|
var modified_file_name = output_json ? output_json['title'] : null;
|
|
if (!output_json) {
|
|
continue;
|
|
}
|
|
|
|
// get filepath with no extension
|
|
const filepath_no_extension = removeFileExtension(output_json['_filename']);
|
|
|
|
var full_file_path = filepath_no_extension + ext;
|
|
var file_name = filepath_no_extension.substring(fileFolderPath.length, filepath_no_extension.length);
|
|
|
|
// renames file if necessary due to bug
|
|
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
|
|
try {
|
|
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
|
|
logger.info('Renamed ' + file_name + '.webm to ' + file_name);
|
|
} catch(e) {
|
|
}
|
|
}
|
|
|
|
if (type === 'audio') {
|
|
let tags = {
|
|
title: output_json['title'],
|
|
artist: output_json['artist'] ? output_json['artist'] : output_json['uploader']
|
|
}
|
|
let success = NodeID3.write(tags, output_json['_filename']);
|
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + output_json['_filename']);
|
|
}
|
|
|
|
// registers file in DB
|
|
file_uid = registerFileDB(full_file_path.substring(fileFolderPath.length, full_file_path.length), type, multiUserMode);
|
|
|
|
if (file_name) file_names.push(file_name);
|
|
}
|
|
|
|
let is_playlist = file_names.length > 1;
|
|
|
|
if (options.merged_string) {
|
|
let current_merged_archive = fs.readFileSync(fileFolderPath + 'merged.txt', 'utf8');
|
|
let diff = current_merged_archive.replace(options.merged_string, '');
|
|
const archive_path = path.join(archivePath, `archive_${type}.txt`);
|
|
fs.appendFileSync(archive_path, diff);
|
|
}
|
|
|
|
download['complete'] = true;
|
|
updateDownloads();
|
|
|
|
var videopathEncoded = encodeURIComponent(file_names[0]);
|
|
|
|
resolve({
|
|
[(type === 'audio') ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded,
|
|
file_names: is_playlist ? file_names : null,
|
|
uid: file_uid
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function downloadFileByURL_normal(url, type, options, sessionID = null) {
|
|
return new Promise(async resolve => {
|
|
var date = Date.now();
|
|
var file_uid = null;
|
|
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath;
|
|
|
|
// prepend with user if needed
|
|
let multiUserMode = null;
|
|
if (options.user) {
|
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
const user_path = path.join(usersFileFolder, options.user, type);
|
|
fs.ensureDirSync(user_path);
|
|
fileFolderPath = user_path + path.sep;
|
|
multiUserMode = {
|
|
user: options.user,
|
|
file_path: fileFolderPath
|
|
}
|
|
options.customFileFolderPath = fileFolderPath;
|
|
}
|
|
|
|
const downloadConfig = await generateArgs(url, type, options);
|
|
|
|
// adds download to download helper
|
|
const download_uid = uuid();
|
|
const session = sessionID ? sessionID : 'undeclared';
|
|
if (!downloads[session]) downloads[session] = {};
|
|
downloads[session][download_uid] = {
|
|
uid: download_uid,
|
|
ui_uid: options.ui_uid,
|
|
downloading: true,
|
|
complete: false,
|
|
url: url,
|
|
type: type,
|
|
percent_complete: 0,
|
|
is_playlist: url.includes('playlist'),
|
|
timestamp_start: Date.now()
|
|
};
|
|
const download = downloads[session][download_uid];
|
|
updateDownloads();
|
|
|
|
const video = youtubedl(url,
|
|
// Optional arguments passed to youtube-dl.
|
|
downloadConfig,
|
|
// Additional options can be given for calling `child_process.execFile()`.
|
|
{ cwd: __dirname });
|
|
|
|
let video_info = null;
|
|
let file_size = 0;
|
|
|
|
// Will be called when the download starts.
|
|
video.on('info', function(info) {
|
|
video_info = info;
|
|
file_size = video_info.size;
|
|
fs.writeJSONSync(removeFileExtension(video_info._filename) + '.info.json', video_info);
|
|
video.pipe(fs.createWriteStream(video_info._filename, { flags: 'w' }))
|
|
});
|
|
// Will be called if download was already completed and there is nothing more to download.
|
|
video.on('complete', function complete(info) {
|
|
'use strict'
|
|
logger.info('file ' + info._filename + ' already downloaded.')
|
|
})
|
|
|
|
let download_pos = 0;
|
|
video.on('data', function data(chunk) {
|
|
download_pos += chunk.length
|
|
// `size` should not be 0 here.
|
|
if (file_size) {
|
|
let percent = (download_pos / file_size * 100).toFixed(2)
|
|
download['percent_complete'] = percent;
|
|
}
|
|
});
|
|
|
|
video.on('end', function() {
|
|
let new_date = Date.now();
|
|
let difference = (new_date - date)/1000;
|
|
logger.debug(`Video download delay: ${difference} seconds.`);
|
|
|
|
download['complete'] = true;
|
|
updateDownloads();
|
|
|
|
// audio-only cleanup
|
|
if (type === 'audio') {
|
|
// filename fix
|
|
video_info['_filename'] = removeFileExtension(video_info['_filename']) + '.mp3';
|
|
|
|
// ID3 tagging
|
|
let tags = {
|
|
title: video_info['title'],
|
|
artist: video_info['artist'] ? video_info['artist'] : video_info['uploader']
|
|
}
|
|
let success = NodeID3.write(tags, video_info._filename);
|
|
if (!success) logger.error('Failed to apply ID3 tag to audio file ' + video_info._filename);
|
|
}
|
|
|
|
// registers file in DB
|
|
const base_file_name = video_info._filename.substring(fileFolderPath.length, video_info._filename.length);
|
|
file_uid = registerFileDB(base_file_name, type, multiUserMode);
|
|
|
|
if (options.merged_string) {
|
|
let current_merged_archive = fs.readFileSync(fileFolderPath + 'merged.txt', 'utf8');
|
|
let diff = current_merged_archive.replace(options.merged_string, '');
|
|
const archive_path = req.isAuthenticated() ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
|
fs.appendFileSync(archive_path, diff);
|
|
}
|
|
|
|
videopathEncoded = encodeURIComponent(removeFileExtension(base_file_name));
|
|
|
|
resolve({
|
|
[(type === 'audio') ? 'audiopathEncoded' : 'videopathEncoded']: videopathEncoded,
|
|
file_names: /*is_playlist ? file_names :*/ null, // playlist support is not ready
|
|
uid: file_uid
|
|
});
|
|
});
|
|
|
|
video.on('error', function error(err) {
|
|
logger.error(err);
|
|
|
|
download[error] = err;
|
|
updateDownloads();
|
|
|
|
resolve(false);
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
async function generateArgs(url, type, options) {
|
|
return new Promise(async resolve => {
|
|
var videopath = '%(title)s';
|
|
var globalArgs = config_api.getConfigItem('ytdl_custom_args');
|
|
var is_audio = type === 'audio';
|
|
|
|
var fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
|
|
|
if (options.customFileFolderPath) fileFolderPath = options.customFileFolderPath;
|
|
|
|
var customArgs = options.customArgs;
|
|
var customOutput = options.customOutput;
|
|
var customQualityConfiguration = options.customQualityConfiguration;
|
|
|
|
// video-specific args
|
|
var selectedHeight = options.selectedHeight;
|
|
|
|
// audio-specific args
|
|
var maxBitrate = options.maxBitrate;
|
|
|
|
var youtubeUsername = options.youtubeUsername;
|
|
var youtubePassword = options.youtubePassword;
|
|
|
|
let downloadConfig = null;
|
|
let qualityPath = is_audio ? '-f bestaudio' :'-f best[ext=mp4]';
|
|
|
|
if (!is_audio && (url.includes('tiktok') || url.includes('pscp.tv'))) {
|
|
// tiktok videos fail when using the default format
|
|
qualityPath = '-f best';
|
|
}
|
|
|
|
if (customArgs) {
|
|
downloadConfig = customArgs.split(' ');
|
|
} else {
|
|
if (customQualityConfiguration) {
|
|
qualityPath = `-f ${customQualityConfiguration}`;
|
|
} else if (selectedHeight && selectedHeight !== '' && !is_audio) {
|
|
qualityPath = `-f '(mp4)[height=${selectedHeight}]'`;
|
|
} else if (maxBitrate && is_audio) {
|
|
qualityPath = `--audio-quality ${maxBitrate}`
|
|
}
|
|
|
|
if (customOutput) {
|
|
downloadConfig = ['-o', path.join(fileFolderPath, customOutput), qualityPath, '--write-info-json', '--print-json'];
|
|
} else {
|
|
downloadConfig = ['-o', path.join(fileFolderPath, videopath + (is_audio ? '.%(ext)s' : '.mp4')), qualityPath, '--write-info-json', '--print-json'];
|
|
}
|
|
|
|
if (is_audio) {
|
|
downloadConfig.push('-x');
|
|
downloadConfig.push('--audio-format', 'mp3');
|
|
}
|
|
|
|
if (youtubeUsername && youtubePassword) {
|
|
downloadConfig.push('--username', youtubeUsername, '--password', youtubePassword);
|
|
}
|
|
|
|
if (!useDefaultDownloadingAgent && customDownloadingAgent) {
|
|
downloadConfig.splice(0, 0, '--external-downloader', customDownloadingAgent);
|
|
}
|
|
|
|
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
|
if (useYoutubeDLArchive) {
|
|
const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
|
// create archive file if it doesn't exist
|
|
if (!fs.existsSync(archive_path)) {
|
|
fs.closeSync(fs.openSync(archive_path, 'w'));
|
|
}
|
|
|
|
let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`);
|
|
// create blacklist file if it doesn't exist
|
|
if (!fs.existsSync(blacklist_path)) {
|
|
fs.closeSync(fs.openSync(blacklist_path, 'w'));
|
|
}
|
|
|
|
let merged_path = fileFolderPath + 'merged.txt';
|
|
fs.ensureFileSync(merged_path);
|
|
// merges blacklist and regular archive
|
|
let inputPathList = [archive_path, blacklist_path];
|
|
let status = await mergeFiles(inputPathList, merged_path);
|
|
|
|
options.merged_string = fs.readFileSync(merged_path, "utf8");
|
|
|
|
downloadConfig.push('--download-archive', merged_path);
|
|
}
|
|
|
|
if (globalArgs && globalArgs !== '') {
|
|
// adds global args
|
|
downloadConfig = downloadConfig.concat(globalArgs.split(' '));
|
|
}
|
|
|
|
}
|
|
resolve(downloadConfig);
|
|
});
|
|
}
|
|
|
|
// currently only works for single urls
|
|
async function getUrlInfos(urls) {
|
|
let startDate = Date.now();
|
|
let result = [];
|
|
return new Promise(resolve => {
|
|
youtubedl.exec(urls.join(' '), ['--dump-json'], {}, (err, output) => {
|
|
let new_date = Date.now();
|
|
let difference = (new_date - startDate)/1000;
|
|
logger.debug(`URL info retrieval delay: ${difference} seconds.`);
|
|
if (err) {
|
|
logger.error('Error during parsing:' + err);
|
|
resolve(null);
|
|
}
|
|
let try_putput = null;
|
|
try {
|
|
try_putput = JSON.parse(output);
|
|
result = try_putput;
|
|
} catch(e) {
|
|
// probably multiple urls
|
|
logger.error('failed to parse for urls starting with ' + urls[0]);
|
|
// logger.info(output);
|
|
}
|
|
resolve(result);
|
|
});
|
|
});
|
|
}
|
|
|
|
function writeToBlacklist(type, line) {
|
|
let blacklistPath = path.join(archivePath, (type === 'audio') ? 'blacklist_audio.txt' : 'blacklist_video.txt');
|
|
// adds newline to the beginning of the line
|
|
line = '\n' + line;
|
|
fs.appendFileSync(blacklistPath, line);
|
|
}
|
|
|
|
// download management functions
|
|
|
|
function updateDownloads() {
|
|
db.assign({downloads: downloads}).write();
|
|
}
|
|
|
|
/*
|
|
function checkDownloads() {
|
|
for (let [session_id, session_downloads] of Object.entries(downloads)) {
|
|
for (let [download_uid, download_obj] of Object.entries(session_downloads)) {
|
|
if (download_obj && !download_obj['complete'] && !download_obj['error']
|
|
&& download_obj.timestamp_start > timestamp_server_start) {
|
|
// download is still running (presumably)
|
|
download_obj.percent_complete = getDownloadPercent(download_obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
function getDownloadPercent(download_obj) {
|
|
if (!download_obj.final_size) {
|
|
if (fs.existsSync(download_obj.expected_json_path)) {
|
|
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8'));
|
|
let calculated_filesize = null;
|
|
if (file_json['format_id']) {
|
|
calculated_filesize = 0;
|
|
const formats_used = file_json['format_id'].split('+');
|
|
for (let i = 0; i < file_json['formats'].length; i++) {
|
|
if (formats_used.includes(file_json['formats'][i]['format_id'])) {
|
|
calculated_filesize += file_json['formats'][i]['filesize'];
|
|
}
|
|
}
|
|
}
|
|
download_obj.final_size = calculated_filesize;
|
|
} else {
|
|
console.log('could not find json file');
|
|
}
|
|
}
|
|
if (fs.existsSync(download_obj.expected_path)) {
|
|
const stats = fs.statSync(download_obj.expected_path);
|
|
const size = stats.size;
|
|
return (size / download_obj.final_size)*100;
|
|
} else {
|
|
console.log('could not find file');
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// youtube-dl functions
|
|
|
|
async function startYoutubeDL() {
|
|
// auto update youtube-dl
|
|
if (!debugMode) await autoUpdateYoutubeDL();
|
|
}
|
|
|
|
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
|
|
async function autoUpdateYoutubeDL() {
|
|
return new Promise(resolve => {
|
|
// get current version
|
|
let current_app_details_path = 'node_modules/youtube-dl/bin/details';
|
|
let current_app_details_exists = fs.existsSync(current_app_details_path);
|
|
if (!current_app_details_exists) {
|
|
logger.error(`Failed to get youtube-dl binary details at location '${current_app_details_path}'. Cancelling update check.`);
|
|
resolve(false);
|
|
return;
|
|
}
|
|
let current_app_details = JSON.parse(fs.readFileSync(current_app_details_path));
|
|
let current_version = current_app_details['version'];
|
|
let stored_binary_path = current_app_details['path'];
|
|
if (!stored_binary_path || typeof stored_binary_path !== 'string') {
|
|
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${current_app_details_path}, attempting to guess actual path...`);
|
|
const guessed_base_path = 'node_modules/youtube-dl/bin/';
|
|
const guessed_file_path = guessed_base_path + 'youtube-dl' + (is_windows ? '.exe' : '');
|
|
if (fs.existsSync(guessed_file_path)) {
|
|
stored_binary_path = guessed_file_path;
|
|
// logger.info('INFO: Guess successful! Update process continuing...')
|
|
} else {
|
|
logger.error(`Guess '${guessed_file_path}' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install.`);
|
|
resolve(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// got version, now let's check the latest version from the youtube-dl API
|
|
let youtubedl_api_path = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags';
|
|
fetch(youtubedl_api_path, {method: 'Get'})
|
|
.then(async res => res.json())
|
|
.then(async (json) => {
|
|
// check if the versions are different
|
|
if (!json || !json[0]) {
|
|
resolve(false);
|
|
return false;
|
|
}
|
|
const latest_update_version = json[0]['name'];
|
|
if (current_version !== latest_update_version) {
|
|
let binary_path = 'node_modules/youtube-dl/bin';
|
|
// versions different, download new update
|
|
logger.info('Found new update for youtube-dl. Updating binary...');
|
|
try {
|
|
await checkExistsWithTimeout(stored_binary_path, 10000);
|
|
} catch(e) {
|
|
logger.error(`Failed to update youtube-dl - ${e}`);
|
|
}
|
|
downloader(binary_path, function error(err, done) {
|
|
if (err) {
|
|
logger.error(err);
|
|
resolve(false);
|
|
}
|
|
logger.info(`Binary successfully updated: ${current_version} -> ${latest_update_version}`);
|
|
resolve(true);
|
|
});
|
|
}
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
async function checkExistsWithTimeout(filePath, timeout) {
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
var timer = setTimeout(function () {
|
|
if (watcher) watcher.close();
|
|
reject(new Error('File did not exists and was not created during the timeout.'));
|
|
}, timeout);
|
|
|
|
fs.access(filePath, fs.constants.R_OK, function (err) {
|
|
if (!err) {
|
|
clearTimeout(timer);
|
|
watcher.close();
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
var dir = path.dirname(filePath);
|
|
var basename = path.basename(filePath);
|
|
var watcher = fs.watch(dir, function (eventType, filename) {
|
|
if (eventType === 'rename' && filename === basename) {
|
|
clearTimeout(timer);
|
|
watcher.close();
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function removeFileExtension(filename) {
|
|
const filename_parts = filename.split('.');
|
|
filename_parts.splice(filename_parts.length - 1)
|
|
return filename_parts.join('.');
|
|
}
|
|
|
|
// https://stackoverflow.com/a/32197381/8088021
|
|
const deleteFolderRecursive = function(folder_to_delete) {
|
|
if (fs.existsSync(folder_to_delete)) {
|
|
fs.readdirSync(folder_to_delete).forEach((file, index) => {
|
|
const curPath = path.join(folder_to_delete, file);
|
|
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
|
deleteFolderRecursive(curPath);
|
|
} else { // delete file
|
|
fs.unlinkSync(curPath);
|
|
}
|
|
});
|
|
fs.rmdirSync(folder_to_delete);
|
|
}
|
|
};
|
|
|
|
app.use(function(req, res, next) {
|
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
|
res.header("Access-Control-Allow-Origin", getOrigin());
|
|
if (req.method === 'OPTIONS') {
|
|
res.sendStatus(200);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
|
|
app.use(function(req, res, next) {
|
|
if (!req.path.includes('/api/')) {
|
|
next();
|
|
} else if (req.query.apiKey === admin_token) {
|
|
next();
|
|
} else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) {
|
|
next();
|
|
} else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) {
|
|
next();
|
|
} else {
|
|
logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`);
|
|
req.socket.end();
|
|
}
|
|
});
|
|
|
|
app.use(compression());
|
|
|
|
const optionalJwt = function (req, res, next) {
|
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
|
if (multiUserMode && ((req.body && req.body.uuid) || (req.query && req.query.uuid)) && (req.path.includes('/api/getFile') ||
|
|
req.path.includes('/api/audio') ||
|
|
req.path.includes('/api/video') ||
|
|
req.path.includes('/api/downloadFile'))) {
|
|
// check if shared video
|
|
const using_body = req.body && req.body.uuid;
|
|
const uuid = using_body ? req.body.uuid : req.query.uuid;
|
|
const uid = using_body ? req.body.uid : req.query.uid;
|
|
const type = using_body ? req.body.type : req.query.type;
|
|
const is_shared = !req.query.id ? auth_api.getUserVideo(uuid, uid, type, true) : auth_api.getUserPlaylist(uuid, req.query.id, null, true);
|
|
if (is_shared) {
|
|
req.can_watch = true;
|
|
return next();
|
|
} else {
|
|
res.sendStatus(401);
|
|
return;
|
|
}
|
|
} else if (multiUserMode && !(req.path.includes('/api/auth/register') && !req.query.jwt)) { // registration should get passed through
|
|
if (!req.query.jwt) {
|
|
res.sendStatus(401);
|
|
return;
|
|
}
|
|
return auth_api.passport.authenticate('jwt', { session: false })(req, res, next);
|
|
}
|
|
return next();
|
|
};
|
|
|
|
app.get('/api/config', function(req, res) {
|
|
let config_file = config_api.getConfigFile();
|
|
res.send({
|
|
config_file: config_file,
|
|
success: !!config_file
|
|
});
|
|
});
|
|
|
|
app.post('/api/setConfig', function(req, res) {
|
|
let new_config_file = req.body.new_config_file;
|
|
if (new_config_file && new_config_file['YoutubeDLMaterial']) {
|
|
let success = config_api.setConfigFile(new_config_file);
|
|
loadConfigValues(); // reloads config values that exist as variables
|
|
res.send({
|
|
success: success
|
|
});
|
|
} else {
|
|
logger.error('Tried to save invalid config file!')
|
|
res.sendStatus(400);
|
|
}
|
|
|
|
});
|
|
|
|
app.get('/api/using-encryption', function(req, res) {
|
|
res.send(usingEncryption);
|
|
});
|
|
|
|
app.post('/api/tomp3', optionalJwt, async function(req, res) {
|
|
var url = req.body.url;
|
|
var options = {
|
|
customArgs: req.body.customArgs,
|
|
customOutput: req.body.customOutput,
|
|
maxBitrate: req.body.maxBitrate,
|
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
|
youtubeUsername: req.body.youtubeUsername,
|
|
youtubePassword: req.body.youtubePassword,
|
|
ui_uid: req.body.ui_uid,
|
|
user: req.isAuthenticated() ? req.user.uid : null
|
|
}
|
|
|
|
const is_playlist = url.includes('playlist');
|
|
if (true || is_playlist)
|
|
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
|
|
else
|
|
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
|
|
if (result_obj) {
|
|
res.send(result_obj);
|
|
} else {
|
|
res.sendStatus(500);
|
|
}
|
|
|
|
res.end("yes");
|
|
});
|
|
|
|
app.post('/api/tomp4', optionalJwt, async function(req, res) {
|
|
var url = req.body.url;
|
|
var options = {
|
|
customArgs: req.body.customArgs,
|
|
customOutput: req.body.customOutput,
|
|
selectedHeight: req.body.selectedHeight,
|
|
customQualityConfiguration: req.body.customQualityConfiguration,
|
|
youtubeUsername: req.body.youtubeUsername,
|
|
youtubePassword: req.body.youtubePassword,
|
|
ui_uid: req.body.ui_uid,
|
|
user: req.isAuthenticated() ? req.user.uid : null
|
|
}
|
|
|
|
const is_playlist = url.includes('playlist');
|
|
let result_obj = null;
|
|
if (is_playlist || options.customQualityConfiguration)
|
|
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
|
|
else
|
|
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
|
|
if (result_obj) {
|
|
res.send(result_obj);
|
|
} else {
|
|
res.sendStatus(500);
|
|
}
|
|
|
|
res.end("yes");
|
|
});
|
|
|
|
// gets the status of the mp3 file that's being downloaded
|
|
app.post('/api/fileStatusMp3', function(req, res) {
|
|
var name = decodeURIComponent(req.body.name + "");
|
|
var exists = "";
|
|
var fullpath = audioFolderPath + name + ".mp3";
|
|
if (fs.existsSync(fullpath)) {
|
|
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
|
|
}
|
|
else
|
|
{
|
|
var percent = 0;
|
|
var size = getFileSizeMp3(name);
|
|
var downloaded = getAmountDownloadedMp3(name);
|
|
if (size > 0)
|
|
percent = downloaded/size;
|
|
exists = ["failed", getFileSizeMp3(name), percent];
|
|
}
|
|
//logger.info(exists + " " + name);
|
|
res.send(exists);
|
|
res.end("yes");
|
|
});
|
|
|
|
// gets the status of the mp4 file that's being downloaded
|
|
app.post('/api/fileStatusMp4', function(req, res) {
|
|
var name = decodeURIComponent(req.body.name);
|
|
var exists = "";
|
|
var fullpath = videoFolderPath + name + ".mp4";
|
|
if (fs.existsSync(fullpath)) {
|
|
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
|
|
} else {
|
|
var percent = 0;
|
|
var size = getFileSizeMp4(name);
|
|
var downloaded = getAmountDownloadedMp4(name);
|
|
if (size > 0)
|
|
percent = downloaded/size;
|
|
exists = ["failed", getFileSizeMp4(name), percent];
|
|
}
|
|
//logger.info(exists + " " + name);
|
|
res.send(exists);
|
|
res.end("yes");
|
|
});
|
|
|
|
// gets all download mp3s
|
|
app.get('/api/getMp3s', optionalJwt, function(req, res) {
|
|
var mp3s = db.get('files.audio').value(); // getMp3s();
|
|
var playlists = db.get('playlists.audio').value();
|
|
const is_authenticated = req.isAuthenticated();
|
|
if (is_authenticated) {
|
|
// get user audio files/playlists
|
|
auth_api.passport.authenticate('jwt')
|
|
mp3s = auth_api.getUserVideos(req.user.uid, 'audio');
|
|
playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
|
|
}
|
|
|
|
res.send({
|
|
mp3s: mp3s,
|
|
playlists: playlists
|
|
});
|
|
});
|
|
|
|
// gets all download mp4s
|
|
app.get('/api/getMp4s', optionalJwt, function(req, res) {
|
|
var mp4s = db.get('files.video').value(); // getMp4s();
|
|
var playlists = db.get('playlists.video').value();
|
|
|
|
const is_authenticated = req.isAuthenticated();
|
|
if (is_authenticated) {
|
|
// get user videos/playlists
|
|
auth_api.passport.authenticate('jwt')
|
|
mp4s = auth_api.getUserVideos(req.user.uid, 'video');
|
|
playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
|
|
}
|
|
|
|
res.send({
|
|
mp4s: mp4s,
|
|
playlists: playlists
|
|
});
|
|
});
|
|
|
|
app.post('/api/getFile', optionalJwt, function (req, res) {
|
|
var uid = req.body.uid;
|
|
var type = req.body.type;
|
|
var uuid = req.body.uuid;
|
|
|
|
var file = null;
|
|
|
|
if (req.isAuthenticated()) {
|
|
file = auth_api.getUserVideo(req.user.uid, uid, type);
|
|
} else if (uuid) {
|
|
file = auth_api.getUserVideo(uuid, uid, type, true);
|
|
} else {
|
|
if (!type) {
|
|
file = db.get('files.audio').find({uid: uid}).value();
|
|
if (!file) {
|
|
file = db.get('files.video').find({uid: uid}).value();
|
|
if (file) type = 'video';
|
|
} else {
|
|
type = 'audio';
|
|
}
|
|
}
|
|
|
|
if (!file && type) file = db.get(`files.${type}`).find({uid: uid}).value();
|
|
}
|
|
|
|
|
|
if (file) {
|
|
res.send({
|
|
success: true,
|
|
file: file
|
|
});
|
|
} else {
|
|
res.send({
|
|
success: false
|
|
});
|
|
}
|
|
});
|
|
|
|
// video sharing
|
|
app.post('/api/enableSharing', optionalJwt, function(req, res) {
|
|
var type = req.body.type;
|
|
var uid = req.body.uid;
|
|
var is_playlist = req.body.is_playlist;
|
|
let success = false;
|
|
// multi-user mode
|
|
if (req.isAuthenticated()) {
|
|
// if multi user mode, use this method instead
|
|
success = auth_api.changeSharingMode(req.user.uid, uid, type, is_playlist, true);
|
|
res.send({success: success});
|
|
return;
|
|
}
|
|
|
|
// single-user mode
|
|
try {
|
|
success = true;
|
|
if (!is_playlist && type !== 'subscription') {
|
|
db.get(`files.${type}`)
|
|
.find({uid: uid})
|
|
.assign({sharingEnabled: true})
|
|
.write();
|
|
} else if (is_playlist) {
|
|
db.get(`playlists.${type}`)
|
|
.find({id: uid})
|
|
.assign({sharingEnabled: true})
|
|
.write();
|
|
} else if (type === 'subscription') {
|
|
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
|
|
// time they are requested from the subscription directory.
|
|
} else {
|
|
// error
|
|
success = false;
|
|
}
|
|
|
|
} catch(err) {
|
|
success = false;
|
|
}
|
|
|
|
res.send({
|
|
success: success
|
|
});
|
|
});
|
|
|
|
app.post('/api/disableSharing', optionalJwt, function(req, res) {
|
|
var type = req.body.type;
|
|
var uid = req.body.uid;
|
|
var is_playlist = req.body.is_playlist;
|
|
|
|
// multi-user mode
|
|
if (req.isAuthenticated()) {
|
|
// if multi user mode, use this method instead
|
|
success = auth_api.changeSharingMode(req.user.uid, uid, type, is_playlist, false);
|
|
res.send({success: success});
|
|
return;
|
|
}
|
|
|
|
// single-user mode
|
|
try {
|
|
success = true;
|
|
if (!is_playlist && type !== 'subscription') {
|
|
db.get(`files.${type}`)
|
|
.find({uid: uid})
|
|
.assign({sharingEnabled: false})
|
|
.write();
|
|
} else if (is_playlist) {
|
|
db.get(`playlists.${type}`)
|
|
.find({id: uid})
|
|
.assign({sharingEnabled: false})
|
|
.write();
|
|
} else if (type === 'subscription') {
|
|
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
|
|
// time they are requested from the subscription directory.
|
|
} else {
|
|
// error
|
|
success = false;
|
|
}
|
|
|
|
} catch(err) {
|
|
success = false;
|
|
}
|
|
|
|
res.send({
|
|
success: success
|
|
});
|
|
});
|
|
|
|
app.post('/api/subscribe', optionalJwt, async (req, res) => {
|
|
let name = req.body.name;
|
|
let url = req.body.url;
|
|
let timerange = req.body.timerange;
|
|
let streamingOnly = req.body.streamingOnly;
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
const new_sub = {
|
|
name: name,
|
|
url: url,
|
|
id: uuid(),
|
|
streamingOnly: streamingOnly,
|
|
user_uid: user_uid
|
|
};
|
|
|
|
// adds timerange if it exists, otherwise all videos will be downloaded
|
|
if (timerange) {
|
|
new_sub.timerange = timerange;
|
|
}
|
|
|
|
const result_obj = await subscriptions_api.subscribe(new_sub, user_uid);
|
|
|
|
if (result_obj.success) {
|
|
res.send({
|
|
new_sub: new_sub
|
|
});
|
|
} else {
|
|
res.send({
|
|
new_sub: null,
|
|
error: result_obj.error
|
|
})
|
|
}
|
|
});
|
|
|
|
app.post('/api/unsubscribe', optionalJwt, async (req, res) => {
|
|
let deleteMode = req.body.deleteMode
|
|
let sub = req.body.sub;
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
let result_obj = subscriptions_api.unsubscribe(sub, deleteMode, user_uid);
|
|
if (result_obj.success) {
|
|
res.send({
|
|
success: result_obj.success
|
|
});
|
|
} else {
|
|
res.send({
|
|
success: false,
|
|
error: result_obj.error
|
|
});
|
|
}
|
|
});
|
|
|
|
app.post('/api/deleteSubscriptionFile', optionalJwt, async (req, res) => {
|
|
let deleteForever = req.body.deleteForever;
|
|
let file = req.body.file;
|
|
let sub = req.body.sub;
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
let success = await subscriptions_api.deleteSubscriptionFile(sub, file, deleteForever, user_uid);
|
|
|
|
if (success) {
|
|
res.send({
|
|
success: success
|
|
});
|
|
} else {
|
|
res.sendStatus(500);
|
|
}
|
|
|
|
});
|
|
|
|
app.post('/api/getSubscription', optionalJwt, async (req, res) => {
|
|
let subID = req.body.id;
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
// get sub from db
|
|
let subscription = subscriptions_api.getSubscription(subID, user_uid);
|
|
|
|
if (!subscription) {
|
|
// failed to get subscription from db, send 400 error
|
|
res.sendStatus(400);
|
|
return;
|
|
}
|
|
|
|
// get sub videos
|
|
if (subscription.name && !subscription.streamingOnly) {
|
|
let base_path = null;
|
|
if (user_uid)
|
|
base_path = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
|
else
|
|
base_path = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
|
|
let appended_base_path = path.join(base_path, subscription.isPlaylist ? 'playlists' : 'channels', subscription.name, '/');
|
|
let files;
|
|
try {
|
|
files = recFindByExt(appended_base_path, 'mp4');
|
|
} catch(e) {
|
|
files = null;
|
|
logger.info('Failed to get folder for subscription: ' + subscription.name + ' at path ' + appended_base_path);
|
|
res.sendStatus(500);
|
|
return;
|
|
}
|
|
var parsed_files = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i];
|
|
var file_path = file.substring(appended_base_path.length, file.length);
|
|
var stats = fs.statSync(file);
|
|
|
|
var id = file_path.substring(0, file_path.length-4);
|
|
var jsonobj = getJSONMp4(id, appended_base_path);
|
|
if (!jsonobj) continue;
|
|
var title = jsonobj.title;
|
|
|
|
var thumbnail = jsonobj.thumbnail;
|
|
var duration = jsonobj.duration;
|
|
var url = jsonobj.webpage_url;
|
|
var uploader = jsonobj.uploader;
|
|
var upload_date = jsonobj.upload_date;
|
|
upload_date = `${upload_date.substring(0, 4)}-${upload_date.substring(4, 6)}-${upload_date.substring(6, 8)}`;
|
|
var size = stats.size;
|
|
|
|
var isaudio = false;
|
|
var file_obj = new File(id, title, thumbnail, isaudio, duration, url, uploader, size, file, upload_date);
|
|
parsed_files.push(file_obj);
|
|
}
|
|
|
|
res.send({
|
|
subscription: subscription,
|
|
files: parsed_files
|
|
});
|
|
} else if (subscription.name && subscription.streamingOnly) {
|
|
// return list of videos
|
|
let parsed_files = [];
|
|
if (subscription.videos) {
|
|
for (let i = 0; i < subscription.videos.length; i++) {
|
|
const video = subscription.videos[i];
|
|
parsed_files.push(new File(video.title, video.title, video.thumbnail, false, video.duration, video.url, video.uploader, video.size, null, null, video.upload_date));
|
|
}
|
|
}
|
|
res.send({
|
|
subscription: subscription,
|
|
files: parsed_files
|
|
});
|
|
} else {
|
|
res.sendStatus(500);
|
|
}
|
|
});
|
|
|
|
app.post('/api/downloadVideosForSubscription', optionalJwt, async (req, res) => {
|
|
let subID = req.body.subID;
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
let sub = subscriptions_api.getSubscription(subID, user_uid);
|
|
subscriptions_api.getVideosForSub(sub, user_uid);
|
|
res.send({
|
|
success: true
|
|
});
|
|
});
|
|
|
|
app.post('/api/getAllSubscriptions', optionalJwt, async (req, res) => {
|
|
let user_uid = req.isAuthenticated() ? req.user.uid : null;
|
|
|
|
// get subs from api
|
|
let subscriptions = subscriptions_api.getAllSubscriptions(user_uid);
|
|
|
|
res.send({
|
|
subscriptions: subscriptions
|
|
});
|
|
});
|
|
|
|
app.post('/api/createPlaylist', optionalJwt, async (req, res) => {
|
|
let playlistName = req.body.playlistName;
|
|
let fileNames = req.body.fileNames;
|
|
let type = req.body.type;
|
|
let thumbnailURL = req.body.thumbnailURL;
|
|
|
|
let new_playlist = {
|
|
'name': playlistName,
|
|
fileNames: fileNames,
|
|
id: shortid.generate(),
|
|
thumbnailURL: thumbnailURL,
|
|
type: type
|
|
};
|
|
|
|
if (req.isAuthenticated()) {
|
|
auth_api.addPlaylist(req.user.uid, new_playlist, type);
|
|
} else {
|
|
db.get(`playlists.${type}`)
|
|
.push(new_playlist)
|
|
.write();
|
|
}
|
|
|
|
|
|
res.send({
|
|
new_playlist: new_playlist,
|
|
success: !!new_playlist // always going to be true
|
|
})
|
|
});
|
|
|
|
app.post('/api/getPlaylist', optionalJwt, async (req, res) => {
|
|
let playlistID = req.body.playlistID;
|
|
let type = req.body.type;
|
|
let uuid = req.body.uuid;
|
|
|
|
let playlist = null;
|
|
|
|
if (req.isAuthenticated()) {
|
|
playlist = auth_api.getUserPlaylist(uuid ? uuid : req.user.uid, playlistID, type);
|
|
type = playlist.type;
|
|
} else {
|
|
if (!type) {
|
|
playlist = db.get('playlists.audio').find({id: playlistID}).value();
|
|
if (!playlist) {
|
|
playlist = db.get('playlists.video').find({id: playlistID}).value();
|
|
if (playlist) type = 'video';
|
|
} else {
|
|
type = 'audio';
|
|
}
|
|
}
|
|
|
|
if (!playlist) playlist = db.get(`playlists.${type}`).find({id: playlistID}).value();
|
|
}
|
|
|
|
res.send({
|
|
playlist: playlist,
|
|
type: type,
|
|
success: !!playlist
|
|
});
|
|
});
|
|
|
|
app.post('/api/updatePlaylist', optionalJwt, async (req, res) => {
|
|
let playlistID = req.body.playlistID;
|
|
let fileNames = req.body.fileNames;
|
|
let type = req.body.type;
|
|
|
|
let success = false;
|
|
try {
|
|
if (req.isAuthenticated()) {
|
|
auth_api.updatePlaylist(req.user.uid, playlistID, fileNames, type);
|
|
} else {
|
|
db.get(`playlists.${type}`)
|
|
.find({id: playlistID})
|
|
.assign({fileNames: fileNames})
|
|
.write();
|
|
}
|
|
|
|
success = true;
|
|
} catch(e) {
|
|
logger.error(`Failed to find playlist with ID ${playlistID}`);
|
|
}
|
|
|
|
res.send({
|
|
success: success
|
|
})
|
|
});
|
|
|
|
app.post('/api/deletePlaylist', optionalJwt, async (req, res) => {
|
|
let playlistID = req.body.playlistID;
|
|
let type = req.body.type;
|
|
|
|
let success = null;
|
|
try {
|
|
if (req.isAuthenticated()) {
|
|
auth_api.removePlaylist(req.user.uid, playlistID, type);
|
|
} else {
|
|
// removes playlist from playlists
|
|
db.get(`playlists.${type}`)
|
|
.remove({id: playlistID})
|
|
.write();
|
|
}
|
|
|
|
success = true;
|
|
} catch(e) {
|
|
success = false;
|
|
}
|
|
|
|
res.send({
|
|
success: success
|
|
})
|
|
});
|
|
|
|
// deletes mp3 file
|
|
app.post('/api/deleteMp3', optionalJwt, async (req, res) => {
|
|
// var name = req.body.name;
|
|
var uid = req.body.uid;
|
|
var blacklistMode = req.body.blacklistMode;
|
|
|
|
if (req.isAuthenticated()) {
|
|
let success = auth_api.deleteUserFile(req.user.uid, uid, 'audio', blacklistMode);
|
|
res.send(success);
|
|
return;
|
|
}
|
|
|
|
var audio_obj = db.get('files.audio').find({uid: uid}).value();
|
|
var name = audio_obj.id;
|
|
var fullpath = audioFolderPath + name + ".mp3";
|
|
var wasDeleted = false;
|
|
if (fs.existsSync(fullpath))
|
|
{
|
|
deleteAudioFile(name, blacklistMode);
|
|
db.get('files.audio').remove({uid: uid}).write();
|
|
wasDeleted = true;
|
|
res.send(wasDeleted);
|
|
res.end("yes");
|
|
} else if (audio_obj) {
|
|
db.get('files.audio').remove({uid: uid}).write();
|
|
wasDeleted = true;
|
|
res.send(wasDeleted);
|
|
} else {
|
|
wasDeleted = false;
|
|
res.send(wasDeleted);
|
|
}
|
|
});
|
|
|
|
// deletes mp4 file
|
|
app.post('/api/deleteMp4', optionalJwt, async (req, res) => {
|
|
var uid = req.body.uid;
|
|
var blacklistMode = req.body.blacklistMode;
|
|
|
|
if (req.isAuthenticated()) {
|
|
let success = auth_api.deleteUserFile(req.user.uid, uid, 'video', blacklistMode);
|
|
res.send(success);
|
|
return;
|
|
}
|
|
|
|
var video_obj = db.get('files.video').find({uid: uid}).value();
|
|
var name = video_obj.id;
|
|
var fullpath = videoFolderPath + name + ".mp4";
|
|
var wasDeleted = false;
|
|
if (fs.existsSync(fullpath))
|
|
{
|
|
wasDeleted = await deleteVideoFile(name, null, blacklistMode);
|
|
db.get('files.video').remove({uid: uid}).write();
|
|
// wasDeleted = true;
|
|
res.send(wasDeleted);
|
|
res.end("yes");
|
|
} else if (video_obj) {
|
|
db.get('files.video').remove({uid: uid}).write();
|
|
wasDeleted = true;
|
|
res.send(wasDeleted);
|
|
} else {
|
|
wasDeleted = false;
|
|
res.send(wasDeleted);
|
|
res.end("yes");
|
|
}
|
|
});
|
|
|
|
app.post('/api/downloadFile', optionalJwt, async (req, res) => {
|
|
let fileNames = req.body.fileNames;
|
|
let zip_mode = req.body.zip_mode;
|
|
let type = req.body.type;
|
|
let outputName = req.body.outputName;
|
|
let fullPathProvided = req.body.fullPathProvided;
|
|
let subscriptionName = req.body.subscriptionName;
|
|
let subscriptionPlaylist = req.body.subPlaylist;
|
|
let file = null;
|
|
if (!zip_mode) {
|
|
fileNames = decodeURIComponent(fileNames);
|
|
const is_audio = type === 'audio';
|
|
const fileFolderPath = is_audio ? audioFolderPath : videoFolderPath;
|
|
const ext = is_audio ? '.mp3' : '.mp4';
|
|
|
|
let base_path = fileFolderPath;
|
|
let usersFileFolder = null;
|
|
if (req.isAuthenticated()) {
|
|
usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
base_path = path.join(usersFileFolder, req.user.uid, type);
|
|
}
|
|
if (!subscriptionName) {
|
|
file = path.join(__dirname, base_path, fileNames + ext);
|
|
} else {
|
|
let basePath = null;
|
|
if (usersFileFolder)
|
|
basePath = path.join(usersFileFolder, req.user.uid, 'subscriptions');
|
|
else
|
|
basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
file = path.join(__dirname, basePath, (subscriptionPlaylist === 'true' ? 'playlists' : 'channels'), subscriptionName, fileNames + '.mp4')
|
|
}
|
|
} else {
|
|
for (let i = 0; i < fileNames.length; i++) {
|
|
fileNames[i] = decodeURIComponent(fileNames[i]);
|
|
}
|
|
file = await createPlaylistZipFile(fileNames, type, outputName, fullPathProvided);
|
|
}
|
|
res.sendFile(file, function (err) {
|
|
if (err) {
|
|
logger.error(err);
|
|
} else if (fullPathProvided) {
|
|
try {
|
|
fs.unlinkSync(file);
|
|
} catch(e) {
|
|
logger.error("Failed to remove file", file);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
app.post('/api/deleteFile', async (req, res) => {
|
|
let fileName = req.body.fileName;
|
|
let type = req.body.type;
|
|
if (type === 'audio') {
|
|
deleteAudioFile(fileName);
|
|
} else if (type === 'video') {
|
|
deleteVideoFile(fileName);
|
|
}
|
|
res.send({});
|
|
});
|
|
|
|
app.post('/api/downloadArchive', async (req, res) => {
|
|
let sub = req.body.sub;
|
|
let archive_dir = sub.archive;
|
|
|
|
let full_archive_path = path.join(__dirname, archive_dir, 'archive.txt');
|
|
|
|
if (fs.existsSync(full_archive_path)) {
|
|
res.sendFile(full_archive_path);
|
|
} else {
|
|
res.sendStatus(404);
|
|
}
|
|
|
|
});
|
|
|
|
// Updater API calls
|
|
|
|
app.get('/api/updaterStatus', async (req, res) => {
|
|
let status = updaterStatus;
|
|
|
|
if (status) {
|
|
res.send(updaterStatus);
|
|
} else {
|
|
res.sendStatus(404);
|
|
}
|
|
|
|
});
|
|
|
|
app.post('/api/updateServer', async (req, res) => {
|
|
let tag = req.body.tag;
|
|
|
|
updateServer(tag);
|
|
|
|
res.send({
|
|
success: true
|
|
});
|
|
|
|
});
|
|
|
|
// Pin API calls
|
|
|
|
app.post('/api/isPinSet', async (req, res) => {
|
|
let stored_pin = db.get('pin_md5').value();
|
|
let is_set = false;
|
|
if (!stored_pin || stored_pin.length === 0) {
|
|
} else {
|
|
is_set = true;
|
|
}
|
|
|
|
res.send({
|
|
is_set: is_set
|
|
});
|
|
});
|
|
|
|
app.post('/api/setPin', async (req, res) => {
|
|
let unhashed_pin = req.body.pin;
|
|
let hashed_pin = md5(unhashed_pin);
|
|
|
|
db.set('pin_md5', hashed_pin).write();
|
|
|
|
res.send({
|
|
success: true
|
|
});
|
|
});
|
|
|
|
app.post('/api/checkPin', async (req, res) => {
|
|
let input_pin = req.body.input_pin;
|
|
let input_pin_md5 = md5(input_pin);
|
|
|
|
let stored_pin = db.get('pin_md5').value();
|
|
|
|
let successful = false;
|
|
|
|
if (input_pin_md5 === stored_pin) {
|
|
successful = true;
|
|
}
|
|
|
|
res.send({
|
|
success: successful
|
|
});
|
|
});
|
|
|
|
// API Key API calls
|
|
|
|
app.post('/api/generateNewAPIKey', function (req, res) {
|
|
const new_api_key = uuid();
|
|
config_api.setConfigItem('ytdl_api_key', new_api_key);
|
|
res.send({new_api_key: new_api_key});
|
|
});
|
|
|
|
// Streaming API calls
|
|
|
|
app.get('/api/video/:id', optionalJwt, function(req , res){
|
|
var head;
|
|
let optionalParams = url_api.parse(req.url,true).query;
|
|
let id = decodeURIComponent(req.params.id);
|
|
let file_path = videoFolderPath + id + '.mp4';
|
|
if (req.isAuthenticated() || req.can_watch) {
|
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
if (optionalParams['subName']) {
|
|
const isPlaylist = optionalParams['subPlaylist'];
|
|
file_path = path.join(usersFileFolder, req.user.uid, 'subscriptions', (isPlaylist === 'true' ? 'playlists/' : 'channels/'),optionalParams['subName'], id + '.mp4')
|
|
} else {
|
|
file_path = path.join(usersFileFolder, req.query.uuid ? req.query.uuid : req.user.uid, 'video', id + '.mp4');
|
|
}
|
|
} else if (optionalParams['subName']) {
|
|
let basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
|
const isPlaylist = optionalParams['subPlaylist'];
|
|
basePath += (isPlaylist === 'true' ? 'playlists/' : 'channels/');
|
|
file_path = basePath + optionalParams['subName'] + '/' + id + '.mp4';
|
|
}
|
|
const stat = fs.statSync(file_path)
|
|
const fileSize = stat.size
|
|
const range = req.headers.range
|
|
if (range) {
|
|
const parts = range.replace(/bytes=/, "").split("-")
|
|
const start = parseInt(parts[0], 10)
|
|
const end = parts[1]
|
|
? parseInt(parts[1], 10)
|
|
: fileSize-1
|
|
const chunksize = (end-start)+1
|
|
const file = fs.createReadStream(file_path, {start, end})
|
|
if (config_api.descriptors[id]) config_api.descriptors[id].push(file);
|
|
else config_api.descriptors[id] = [file];
|
|
file.on('close', function() {
|
|
let index = config_api.descriptors[id].indexOf(file);
|
|
config_api.descriptors[id].splice(index, 1);
|
|
logger.debug('Successfully closed stream and removed file reference.');
|
|
});
|
|
head = {
|
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
'Accept-Ranges': 'bytes',
|
|
'Content-Length': chunksize,
|
|
'Content-Type': 'video/mp4',
|
|
}
|
|
res.writeHead(206, head);
|
|
file.pipe(res);
|
|
} else {
|
|
head = {
|
|
'Content-Length': fileSize,
|
|
'Content-Type': 'video/mp4',
|
|
}
|
|
res.writeHead(200, head)
|
|
fs.createReadStream(file_path).pipe(res)
|
|
}
|
|
});
|
|
|
|
app.get('/api/audio/:id', optionalJwt, function(req , res){
|
|
var head;
|
|
let id = decodeURIComponent(req.params.id);
|
|
let file_path = "audio/" + id + '.mp3';
|
|
if (req.isAuthenticated()) {
|
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
file_path = path.join(usersFileFolder, req.user.name, 'audio', id + '.mp3');
|
|
}
|
|
file_path = file_path.replace(/\"/g, '\'');
|
|
const stat = fs.statSync(file_path)
|
|
const fileSize = stat.size
|
|
const range = req.headers.range
|
|
if (range) {
|
|
const parts = range.replace(/bytes=/, "").split("-")
|
|
const start = parseInt(parts[0], 10)
|
|
const end = parts[1]
|
|
? parseInt(parts[1], 10)
|
|
: fileSize-1
|
|
const chunksize = (end-start)+1
|
|
const file = fs.createReadStream(file_path, {start, end});
|
|
if (config_api.descriptors[id]) config_api.descriptors[id].push(file);
|
|
else config_api.descriptors[id] = [file];
|
|
file.on('close', function() {
|
|
let index = config_api.descriptors[id].indexOf(file);
|
|
config_api.descriptors[id].splice(index, 1);
|
|
logger.debug('Successfully closed stream and removed file reference.');
|
|
});
|
|
head = {
|
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
'Accept-Ranges': 'bytes',
|
|
'Content-Length': chunksize,
|
|
'Content-Type': 'audio/mp3',
|
|
}
|
|
res.writeHead(206, head);
|
|
file.pipe(res);
|
|
} else {
|
|
head = {
|
|
'Content-Length': fileSize,
|
|
'Content-Type': 'audio/mp3',
|
|
}
|
|
res.writeHead(200, head)
|
|
fs.createReadStream(file_path).pipe(res)
|
|
}
|
|
});
|
|
|
|
// Downloads management
|
|
|
|
app.get('/api/downloads', async (req, res) => {
|
|
res.send({downloads: downloads});
|
|
});
|
|
|
|
app.post('/api/download', async (req, res) => {
|
|
var session_id = req.body.session_id;
|
|
var download_id = req.body.download_id;
|
|
let found_download = null;
|
|
|
|
// find download
|
|
if (downloads[session_id] && Object.keys(downloads[session_id])) {
|
|
let session_downloads = Object.values(downloads[session_id]);
|
|
for (let i = 0; i < session_downloads.length; i++) {
|
|
let session_download = session_downloads[i];
|
|
if (session_download && session_download['ui_uid'] === download_id) {
|
|
found_download = session_download;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (found_download) {
|
|
res.send({download: found_download});
|
|
} else {
|
|
res.send({download: null});
|
|
}
|
|
});
|
|
|
|
app.post('/api/clearDownloads', async (req, res) => {
|
|
let success = false;
|
|
var delete_all = req.body.delete_all;
|
|
if (!req.body.session_id) req.body.session_id = 'undeclared';
|
|
var session_id = req.body.session_id;
|
|
var download_id = req.body.download_id;
|
|
if (delete_all) {
|
|
// delete all downloads
|
|
downloads = {};
|
|
success = true;
|
|
} else if (download_id) {
|
|
// delete just 1 download
|
|
if (downloads[session_id][download_id]) {
|
|
delete downloads[session_id][download_id];
|
|
success = true;
|
|
} else if (!downloads[session_id]) {
|
|
logger.error(`Session ${session_id} has no downloads.`)
|
|
} else if (!downloads[session_id][download_id]) {
|
|
logger.error(`Download '${download_id}' for session '${session_id}' could not be found`);
|
|
}
|
|
} else if (session_id) {
|
|
// delete a session's downloads
|
|
if (downloads[session_id]) {
|
|
delete downloads[session_id];
|
|
success = true;
|
|
} else {
|
|
logger.error(`Session ${session_id} has no downloads.`)
|
|
}
|
|
}
|
|
updateDownloads();
|
|
res.send({success: success, downloads: downloads});
|
|
});
|
|
|
|
app.post('/api/getVideoInfos', async (req, res) => {
|
|
let fileNames = req.body.fileNames;
|
|
let urlMode = !!req.body.urlMode;
|
|
let type = req.body.type;
|
|
let result = null;
|
|
if (!urlMode) {
|
|
if (type === 'audio') {
|
|
result = getAudioInfos(fileNames)
|
|
} else if (type === 'video') {
|
|
result = getVideoInfos(fileNames);
|
|
}
|
|
} else {
|
|
result = await getUrlInfos(fileNames);
|
|
}
|
|
res.send({
|
|
result: result,
|
|
success: !!result
|
|
})
|
|
});
|
|
|
|
// user authentication
|
|
|
|
app.post('/api/auth/register'
|
|
, optionalJwt
|
|
, auth_api.registerUser);
|
|
app.post('/api/auth/login'
|
|
, auth_api.passport.authenticate('local', {})
|
|
, auth_api.passport.authorize('local')
|
|
, auth_api.generateJWT
|
|
, auth_api.returnAuthResponse
|
|
);
|
|
app.post('/api/auth/jwtAuth'
|
|
, auth_api.passport.authenticate('jwt', { session: false })
|
|
, auth_api.passport.authorize('jwt')
|
|
, auth_api.generateJWT
|
|
, auth_api.returnAuthResponse
|
|
);
|
|
app.post('/api/auth/changePassword', optionalJwt, async (req, res) => {
|
|
let user_uid = req.user.uid;
|
|
let password = req.body.new_password;
|
|
let success = await auth_api.changeUserPassword(user_uid, password);
|
|
res.send({success: success});
|
|
});
|
|
app.post('/api/auth/adminExists', async (req, res) => {
|
|
let exists = auth_api.adminExists();
|
|
res.send({exists: exists});
|
|
});
|
|
|
|
// user management
|
|
app.post('/api/getUsers', optionalJwt, async (req, res) => {
|
|
let users = users_db.get('users').value();
|
|
res.send({users: users});
|
|
});
|
|
app.post('/api/getRoles', optionalJwt, async (req, res) => {
|
|
let roles = users_db.get('roles').value();
|
|
res.send({roles: roles});
|
|
});
|
|
|
|
app.post('/api/changeUser', optionalJwt, async (req, res) => {
|
|
let change_obj = req.body.change_object;
|
|
try {
|
|
const user_db_obj = users_db.get('users').find({uid: change_obj.uid});
|
|
if (change_obj.name) {
|
|
user_db_obj.assign({name: change_obj.name}).write();
|
|
}
|
|
if (change_obj.role) {
|
|
user_db_obj.assign({role: change_obj.role}).write();
|
|
}
|
|
res.send({success: true});
|
|
} catch (err) {
|
|
logger.error(err);
|
|
res.send({success: false});
|
|
}
|
|
});
|
|
|
|
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
|
let uid = req.body.uid;
|
|
try {
|
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
|
const user_db_obj = users_db.get('users').find({uid: uid});
|
|
if (user_db_obj.value()) {
|
|
// user exists, let's delete
|
|
deleteFolderRecursive(user_folder);
|
|
users_db.get('users').remove({uid: uid}).write();
|
|
}
|
|
res.send({success: true});
|
|
} catch (err) {
|
|
logger.error(err);
|
|
res.send({success: false});
|
|
}
|
|
});
|
|
|
|
app.post('/api/changeUserPermissions', optionalJwt, async (req, res) => {
|
|
const user_uid = req.body.user_uid;
|
|
const permission = req.body.permission;
|
|
const new_value = req.body.new_value;
|
|
|
|
if (!permission || !new_value) {
|
|
res.sendStatus(400);
|
|
return;
|
|
}
|
|
|
|
const success = auth_api.changeUserPermissions(user_uid, permission, new_value);
|
|
|
|
res.send({success: success});
|
|
});
|
|
|
|
app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
|
|
const role = req.body.role;
|
|
const permission = req.body.permission;
|
|
const new_value = req.body.new_value;
|
|
|
|
if (!permission || !new_value) {
|
|
res.sendStatus(400);
|
|
return;
|
|
}
|
|
|
|
const success = auth_api.changeRolePermissions(role, permission, new_value);
|
|
|
|
res.send({success: success});
|
|
});
|
|
|
|
app.use(function(req, res, next) {
|
|
//if the request is not html then move along
|
|
var accept = req.accepts('html', 'json', 'xml');
|
|
if (accept !== 'html') {
|
|
return next();
|
|
}
|
|
|
|
// if the request has a '.' assume that it's for a file, move along
|
|
var ext = path.extname(req.path);
|
|
if (ext !== '') {
|
|
return next();
|
|
}
|
|
|
|
let index_path = path.join(__dirname, 'public', 'index.html');
|
|
|
|
fs.createReadStream(index_path).pipe(res);
|
|
|
|
});
|
|
|
|
let public_dir = path.join(__dirname, 'public');
|
|
|
|
app.use(express.static(public_dir));
|