const { uuid } = require ( 'uuidv4' ) ;
var fs = require ( 'fs-extra' ) ;
var { promisify } = require ( 'util' ) ;
var auth _api = require ( './authentication/auth' ) ;
var winston = require ( 'winston' ) ;
var path = require ( 'path' ) ;
var youtubedl = require ( 'youtube-dl' ) ;
var ffmpeg = require ( 'fluent-ffmpeg' ) ;
var compression = require ( 'compression' ) ;
var glob = require ( "glob" )
var multer = require ( 'multer' ) ;
var express = require ( "express" ) ;
var bodyParser = require ( "body-parser" ) ;
var archiver = require ( 'archiver' ) ;
var unzipper = require ( 'unzipper' ) ;
var db _api = require ( './db' ) ;
var utils = require ( './utils' )
var mergeFiles = require ( 'merge-files' ) ;
const low = require ( 'lowdb' )
var ProgressBar = require ( 'progress' ) ;
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' )
var categories _api = require ( './categories' ) ;
var twitch _api = require ( './twitch' ) ;
const CONSTS = require ( './consts' )
const { spawn } = require ( 'child_process' )
const read _last _lines = require ( 'read-last-lines' ) ;
var ps = require ( 'ps-node' ) ;
const is _windows = process . platform === 'win32' ;
var app = express ( ) ;
// database setup
const FileSync = require ( 'lowdb/adapters/FileSync' ) ;
const config = require ( './config.js' ) ;
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 ) ;
// env var setup
const umask = process . env . YTDL _UMASK ;
if ( umask ) process . umask ( parseInt ( umask ) ) ;
// 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 . initialize ( logger ) ;
auth _api . initialize ( users _db , logger ) ;
db _api . initialize ( db , users _db , logger ) ;
subscriptions _api . initialize ( db , users _db , logger , db _api ) ;
categories _api . initialize ( db , users _db , logger , db _api ) ;
// Set some defaults
db . defaults (
{
playlists : [ ] ,
files : [ ] ,
configWriteFlag : false ,
downloads : { } ,
subscriptions : [ ] ,
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 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 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 (commented out b/c of repo takedown)
// startYoutubeDL();
var validDownloadingAgents = [
'aria2c' ,
'avconv' ,
'axel' ,
'curl' ,
'ffmpeg' ,
'httpie' ,
'wget'
] ;
const subscription _timeouts = { } ;
// don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
let writeConfigMode = process . env . write _ytdl _config ;
// 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 ( ) ) ;
// actual functions
/ * *
* setTimeout , but its a promise .
* @ param { number } ms
* /
async function wait ( ms ) {
await new Promise ( resolve => {
setTimeout ( resolve , ms ) ;
} ) ;
}
async function checkMigrations ( ) {
// 3.5->3.6 migration
const files _to _db _migration _complete = true ; // migration phased out! previous code: db.get('files_to_db_migration_complete').value();
if ( ! files _to _db _migration _complete ) {
logger . info ( 'Beginning migration: 3.5->3.6+' )
const success = await runFilesToDBMigration ( )
if ( success ) { logger . info ( '3.5->3.6+ migration complete!' ) ; }
else { logger . error ( 'Migration failed: 3.5->3.6+' ) ; }
}
// 4.1->4.2 migration
const add _description _migration _complete = db . get ( 'add_description_migration_complete' ) . value ( ) ;
if ( ! add _description _migration _complete ) {
logger . info ( 'Beginning migration: 4.1->4.2+' )
let success = await simplifyDBFileStructure ( ) ;
success = success && await addMetadataPropertyToDB ( 'view_count' ) ;
success = success && await addMetadataPropertyToDB ( 'description' ) ;
success = success && await addMetadataPropertyToDB ( 'height' ) ;
success = success && await addMetadataPropertyToDB ( 'abr' ) ;
if ( success ) { logger . info ( '4.1->4.2+ migration complete!' ) ; }
else { logger . error ( 'Migration failed: 4.1->4.2+' ) ; }
}
return true ;
}
async function runFilesToDBMigration ( ) {
try {
let mp3s = await getMp3s ( ) ;
let mp4s = await 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 } ` ) ;
await db _api . 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 } ` ) ;
await db _api . registerFileDB ( file _obj . id + '.mp4' , 'video' ) ;
}
}
// sets migration to complete
db . set ( 'files_to_db_migration_complete' , true ) . write ( ) ;
return true ;
} catch ( err ) {
logger . error ( err ) ;
return false ;
}
}
async function simplifyDBFileStructure ( ) {
let users = users _db . get ( 'users' ) . value ( ) ;
for ( let i = 0 ; i < users . length ; i ++ ) {
const user = users [ i ] ;
if ( user [ 'files' ] [ 'video' ] !== undefined && user [ 'files' ] [ 'audio' ] !== undefined ) {
const user _files = user [ 'files' ] [ 'video' ] . concat ( user [ 'files' ] [ 'audio' ] ) ;
const user _db _path = users _db . get ( 'users' ) . find ( { uid : user [ 'uid' ] } ) ;
user _db _path . assign ( { files : user _files } ) . write ( ) ;
}
if ( user [ 'playlists' ] [ 'video' ] !== undefined && user [ 'playlists' ] [ 'audio' ] !== undefined ) {
const user _playlists = user [ 'playlists' ] [ 'video' ] . concat ( user [ 'playlists' ] [ 'audio' ] ) ;
const user _db _path = users _db . get ( 'users' ) . find ( { uid : user [ 'uid' ] } ) ;
user _db _path . assign ( { playlists : user _playlists } ) . write ( ) ;
}
}
if ( db . get ( 'files.video' ) . value ( ) !== undefined && db . get ( 'files.audio' ) . value ( ) !== undefined ) {
const files = db . get ( 'files.video' ) . value ( ) . concat ( db . get ( 'files.audio' ) ) ;
db . assign ( { files : files } ) . write ( ) ;
}
if ( db . get ( 'playlists.video' ) . value ( ) !== undefined && db . get ( 'playlists.audio' ) . value ( ) !== undefined ) {
const playlists = db . get ( 'playlists.video' ) . value ( ) . concat ( db . get ( 'playlists.audio' ) ) ;
db . assign ( { playlists : playlists } ) . write ( ) ;
}
return true ;
}
async function addMetadataPropertyToDB ( property _key ) {
try {
const dirs _to _check = db _api . getFileDirectoriesAndDBs ( ) ;
for ( const dir _to _check of dirs _to _check ) {
// recursively get all files in dir's path
const files = await utils . getDownloadedFilesByType ( dir _to _check . basePath , dir _to _check . type , true ) ;
for ( const file of files ) {
if ( file [ property _key ] ) {
dir _to _check . dbPath . find ( { id : file . id } ) . assign ( { [ property _key ] : file [ property _key ] } ) . write ( ) ;
}
}
}
// sets migration to complete
db . set ( 'add_description_migration_complete' , true ) . write ( ) ;
return true ;
} catch ( err ) {
logger . error ( err ) ;
return 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 ( ) ;
}
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 ( ) {
var child _process = require ( 'child_process' ) ;
var exec = promisify ( child _process . exec ) ;
await exec ( 'npm install' , { stdio : [ 0 , 1 , 2 ] } ) ;
return true ;
}
async function backupServerLite ( ) {
await fs . ensureDir ( 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 ) ) ;
await new Promise ( resolve => {
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
} ) ;
resolve ( archive . finalize ( ) ) ;
} ) ;
// wait a tiny bit for the zip to reload in fs
await wait ( 100 ) ;
return true ;
}
async function isNewVersionAvailable ( ) {
// 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 ) {
return true ;
} else {
return false ;
}
}
async function getLatestVersion ( ) {
const res = await fetch ( 'https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest' , { method : 'Get' } ) ;
const json = await res . 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' ] } ` )
}
return json [ 'tag_name' ] ;
}
async function killAllDownloads ( ) {
const lookupAsync = promisify ( ps . lookup ) ;
try {
await lookupAsync ( {
command : 'youtube-dl'
} ) ;
} catch ( err ) {
// failed to get list of processes
logger . error ( 'Failed to get a list of running youtube-dl processes.' ) ;
logger . error ( err ) ;
return {
details : err ,
success : false
} ;
}
// processes that contain the string 'youtube-dl' in the name will be looped
resultList . forEach ( function ( process ) {
if ( process ) {
ps . kill ( process . pid , 'SIGKILL' , function ( err ) {
if ( err ) {
// failed to kill, process may have ended on its own
logger . warn ( ` Failed to kill process with PID ${ process . pid } ` ) ;
logger . warn ( err ) ;
}
else {
logger . verbose ( ` Process ${ process . pid } has been killed! ` ) ;
}
} ) ;
}
} ) ;
return {
success : true
} ;
}
async function setPortItemFromENV ( ) {
config _api . setConfigItem ( 'ytdl_port' , backendPort . toString ( ) ) ;
await wait ( 100 ) ;
return true ;
}
async function setAndLoadConfig ( ) {
await setConfigFromEnv ( ) ;
await loadConfig ( ) ;
}
async function setConfigFromEnv ( ) {
let config _items = getEnvConfigItems ( ) ;
let success = config _api . setConfigItems ( config _items ) ;
if ( success ) {
logger . info ( 'Config items set using ENV variables.' ) ;
await wait ( 100 ) ;
return true ;
} else {
logger . error ( 'ERROR: Failed to set config items using ENV variables.' ) ;
return false ;
}
}
async function loadConfig ( ) {
loadConfigValues ( ) ;
// creates archive path if missing
await fs . ensureDir ( archivePath ) ;
// check migrations
await checkMigrations ( ) ;
// now this is done here due to youtube-dl's repo takedown
await startYoutubeDL ( ) ;
// get subscriptions
if ( allowSubscriptions ) {
// runs initially, then runs every ${subscriptionCheckInterval} seconds
watchSubscriptions ( ) ;
setInterval ( ( ) => {
watchSubscriptions ( ) ;
} , subscriptionsCheckInterval * 1000 ) ;
}
db _api . importUnregisteredFiles ( ) ;
// load in previous downloads
downloads = db . get ( 'downloads' ) . value ( ) ;
// start the server here
startServer ( ) ;
return true ;
}
function loadConfigValues ( ) {
url = ! debugMode ? config _api . getConfigItem ( 'ytdl_url' ) : 'http://localhost:4200' ;
backendPort = config _api . getConfigItem ( 'ytdl_port' ) ;
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 ;
}
// empty url defaults to default URL
if ( ! url || url === '' ) url = 'http://example.com'
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 ( subscriptions _amount ) {
// frequency is once every 5 mins by default
let interval _in _ms = subscriptionsCheckInterval * 1000 ;
const subinterval _in _ms = interval _in _ms / subscriptions _amount ;
return subinterval _in _ms ;
}
async 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 ] ;
// don't check the sub if the last check for the same subscription has not completed
if ( subscription _timeouts [ sub . id ] ) {
logger . verbose ( ` Subscription: skipped checking ${ sub . name } as the last check for ${ sub . name } has not completed. ` ) ;
continue ;
}
if ( ! sub . name ) {
logger . verbose ( ` Subscription: skipped check for subscription with uid ${ sub . id } as name has not been retrieved yet. ` ) ;
continue ;
}
logger . verbose ( 'Watching ' + sub . name + ' with delay interval of ' + delay _interval ) ;
setTimeout ( async ( ) => {
const multiUserModeChanged = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) !== multiUserMode ;
if ( multiUserModeChanged ) {
logger . verbose ( ` Skipping subscription ${ sub . name } due to multi-user mode change. ` ) ;
return ;
}
await subscriptions _api . getVideosForSub ( sub , sub . user _uid ) ;
subscription _timeouts [ sub . id ] = false ;
} , current _delay ) ;
subscription _timeouts [ sub . id ] = true ;
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 getThumbnailMp3 ( name )
{
var obj = utils . getJSONMp3 ( name , audioFolderPath ) ;
var thumbnailLink = obj . thumbnail ;
return thumbnailLink ;
}
function getThumbnailMp4 ( name )
{
var obj = utils . getJSONMp4 ( name , videoFolderPath ) ;
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 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 , user _uid = null ) {
let zipFolderPath = null ;
if ( ! fullPathProvided ) {
zipFolderPath = path . join ( _ _dirname , ( type === 'audio' ) ? audioFolderPath : videoFolderPath ) ;
if ( user _uid ) zipFolderPath = path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , zipFolderPath ) ;
} 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 ? path . join ( 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
await wait ( 100 ) ;
return path . join ( zipFolderPath , outputName + '.zip' ) ;
}
async function deleteAudioFile ( name , customPath = null , blacklistMode = false ) {
let filePath = customPath ? customPath : audioFolderPath ;
var jsonPath = path . join ( filePath , name + '.mp3.info.json' ) ;
var altJSONPath = path . join ( filePath , name + '.info.json' ) ;
var audioFilePath = path . join ( filePath , name + '.mp3' ) ;
var thumbnailPath = path . join ( filePath , name + '.webp' ) ;
var altThumbnailPath = path . join ( filePath , name + '.jpg' ) ;
jsonPath = path . join ( _ _dirname , jsonPath ) ;
altJSONPath = path . join ( _ _dirname , altJSONPath ) ;
audioFilePath = path . join ( _ _dirname , audioFilePath ) ;
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 audioFileExists = await fs . pathExists ( 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 = await utils . getJSONMp3 ( name , filePath ) ;
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 subscriptions _api . removeIDFromArchive ( archive _path , id ) : null ;
if ( blacklistMode && line ) await writeToBlacklist ( 'audio' , 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 ) ;
if ( audioFileExists ) {
await fs . unlink ( audioFilePath ) ;
if ( await fs . pathExists ( jsonPath ) || await fs . pathExists ( audioFilePath ) ) {
return false ;
} else {
return true ;
}
} else {
// TODO: tell user that the file didn't exist
return true ;
}
}
async function deleteVideoFile ( name , customPath = null , blacklistMode = false ) {
let filePath = customPath ? customPath : videoFolderPath ;
var jsonPath = path . join ( filePath , name + '.info.json' ) ;
var altJSONPath = path . join ( filePath , name + '.mp4.info.json' ) ;
var videoFilePath = path . join ( filePath , name + '.mp4' ) ;
var thumbnailPath = path . join ( filePath , name + '.webp' ) ;
var altThumbnailPath = path . join ( filePath , name + '.jpg' ) ;
jsonPath = path . join ( _ _dirname , jsonPath ) ;
videoFilePath = path . join ( _ _dirname , videoFilePath ) ;
let jsonExists = await fs . pathExists ( jsonPath ) ;
let videoFileExists = await fs . pathExists ( videoFilePath ) ;
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 ;
}
}
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 = await utils . getJSONMp4 ( name , filePath ) ;
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 subscriptions _api . removeIDFromArchive ( archive _path , id ) : null ;
if ( blacklistMode && line ) await writeToBlacklist ( 'video' , line ) ;
} else {
logger . info ( 'Could not find archive file for videos. Creating...' ) ;
fs . closeSync ( fs . openSync ( archive _path , 'w' ) ) ;
}
}
if ( jsonExists ) await fs . unlink ( jsonPath ) ;
if ( thumbnailExists ) await fs . unlink ( thumbnailPath ) ;
if ( videoFileExists ) {
await fs . unlink ( videoFilePath ) ;
if ( await fs . pathExists ( jsonPath ) || await fs . pathExists ( videoFilePath ) ) {
return false ;
} else {
return true ;
}
} else {
// TODO: tell user that the file didn't exist
return true ;
}
}
/ * *
* @ param { 'audio' | 'video' } type
* @ param { string [ ] } fileNames
* /
async function getAudioOrVideoInfos ( type , fileNames ) {
let result = await Promise . all ( fileNames . map ( async fileName => {
let fileLocation = videoFolderPath + fileName ;
if ( type === 'audio' ) {
fileLocation += '.mp3.info.json' ;
} else if ( type === 'video' ) {
fileLocation += '.info.json' ;
}
if ( await fs . pathExists ( fileLocation ) ) {
let data = await fs . readFile ( fileLocation ) ;
try {
return JSON . parse ( data ) ;
} catch ( e ) {
let suffix ;
if ( type === 'audio' ) {
suffix += '.mp3' ;
} else if ( type === 'video' ) {
suffix += '.mp4' ;
}
logger . error ( ` Could not find info for file ${ fileName } ${ suffix } ` ) ;
}
}
return null ;
} ) ) ;
return result . filter ( data => data != null ) ;
}
// 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 ;
let category = null ;
// 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 ;
}
options . downloading _method = 'exec' ;
let 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 ( ) ,
filesize : null
} ;
const download = downloads [ session ] [ download _uid ] ;
updateDownloads ( ) ;
// get video info prior to download
let info = await getVideoInfoByURL ( url , downloadConfig , download ) ;
if ( ! info ) {
resolve ( false ) ;
return ;
} else {
// check if it fits into a category. If so, then get info again using new downloadConfig
category = await categories _api . categorize ( info ) ;
// set custom output if the category has one and re-retrieve info so the download manager has the right file name
if ( category && category [ 'custom_output' ] ) {
options . customOutput = category [ 'custom_output' ] ;
options . noRelativePath = true ;
downloadConfig = await generateArgs ( url , type , options ) ;
info = await getVideoInfoByURL ( url , downloadConfig , download ) ;
}
// store info in download for future use
download [ '_filename' ] = info [ '_filename' ] ;
download [ 'filesize' ] = utils . getExpectedFileSize ( info ) ;
}
const download _checker = setInterval ( ( ) => checkDownloadPercent ( download ) , 1000 ) ;
// download file
youtubedl . exec ( url , downloadConfig , { } , function ( err , output ) {
clearInterval ( download _checker ) ; // stops the download checker from running as the download finished (or errored)
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 ) ;
return ;
} else if ( output ) {
if ( output . length === 0 || output [ 0 ] . length === 0 ) {
download [ 'error' ] = 'No output. Check if video already exists in your archive.' ;
logger . warn ( ` No output received for video download, check if it 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 ;
}
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 ) ;
if ( type === 'video' && url . includes ( 'twitch.tv/videos/' ) && url . split ( 'twitch.tv/videos/' ) . length > 1
&& config . getConfigItem ( 'ytdl_use_twitch_api' ) && config . getConfigItem ( 'ytdl_twitch_auto_download_chat' ) ) {
let vodId = url . split ( 'twitch.tv/videos/' ) [ 1 ] ;
vodId = vodId . split ( '?' ) [ 0 ] ;
twitch _api . downloadTwitchChatByVODID ( vodId , file _name , type , options . user ) ;
}
// 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' ] ) ;
}
const file _path = options . noRelativePath ? path . basename ( full _file _path ) : full _file _path . substring ( fileFolderPath . length , full _file _path . length ) ;
const customPath = options . noRelativePath ? path . dirname ( full _file _path ) . split ( path . sep ) . pop ( ) : null ;
// registers file in DB
file _uid = db _api . registerFileDB ( file _path , type , multiUserMode , null , customPath ) ;
if ( file _name ) file _names . push ( file _name ) ;
}
let is _playlist = file _names . length > 1 ;
if ( options . merged _string !== null && options . merged _string !== undefined ) {
let current _merged _archive = fs . readFileSync ( path . join ( fileFolderPath , ` merged_ ${ type } .txt ` ) , 'utf8' ) ;
let diff = current _merged _archive . replace ( options . merged _string , '' ) ;
const archive _path = options . user ? path . join ( fileFolderPath , 'archives' , ` archive_ ${ type } .txt ` ) : path . join ( archivePath , ` archive_ ${ type } .txt ` ) ;
fs . appendFileSync ( archive _path , diff ) ;
}
download [ 'complete' ] = true ;
download [ 'fileNames' ] = is _playlist ? file _names : [ full _file _path ]
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 ;
const is _audio = type === 'audio' ;
const ext = is _audio ? '.mp3' : '.mp4' ;
var fileFolderPath = is _audio ? audioFolderPath : videoFolderPath ;
if ( is _audio && url . includes ( 'youtu' ) ) { options . skip _audio _args = true ; }
// 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 ;
}
options . downloading _method = 'normal' ;
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 ;
const json _path = removeFileExtension ( video _info . _filename ) + '.info.json' ;
fs . ensureFileSync ( json _path ) ;
fs . writeJSONSync ( json _path , 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' , async function ( ) {
let new _date = Date . now ( ) ;
let difference = ( new _date - date ) / 1000 ;
logger . debug ( ` Video download delay: ${ difference } seconds. ` ) ;
download [ 'timestamp_end' ] = Date . now ( ) ;
download [ 'fileNames' ] = [ removeFileExtension ( video _info . _filename ) + ext ] ;
download [ 'complete' ] = true ;
updateDownloads ( ) ;
// audio-only cleanup
if ( is _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 ) ;
const possible _webm _path = removeFileExtension ( video _info [ '_filename' ] ) + '.webm' ;
const possible _mp4 _path = removeFileExtension ( video _info [ '_filename' ] ) + '.mp4' ;
// check if audio file is webm
if ( fs . existsSync ( possible _webm _path ) ) await convertFileToMp3 ( possible _webm _path , video _info [ '_filename' ] ) ;
else if ( fs . existsSync ( possible _mp4 _path ) ) await convertFileToMp3 ( possible _mp4 _path , video _info [ '_filename' ] ) ;
}
// registers file in DB
const base _file _name = video _info . _filename . substring ( fileFolderPath . length , video _info . _filename . length ) ;
file _uid = db _api . registerFileDB ( base _file _name , type , multiUserMode ) ;
if ( options . merged _string !== null && options . merged _string !== undefined ) {
let current _merged _archive = fs . readFileSync ( path . join ( fileFolderPath , ` merged_ ${ type } .txt ` ) , 'utf8' ) ;
let diff = current _merged _archive . replace ( options . merged _string , '' ) ;
const archive _path = options . user ? 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 ( {
[ is _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 ) {
var videopath = config _api . getConfigItem ( 'ytdl_default_file_output' ) ? config _api . getConfigItem ( 'ytdl_default_file_output' ) : '%(title)s' ;
var globalArgs = config _api . getConfigItem ( 'ytdl_custom_args' ) ;
let useCookies = config _api . getConfigItem ( 'ytdl_use_cookies' ) ;
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 && ! options . skip _audio _args ) ? [ '-f' , 'bestaudio' ] : [ '-f' , 'bestvideo+bestaudio' , '--merge-output-format' , 'mp4' ] ;
const is _youtube = url . includes ( 'youtu' ) ;
if ( ! is _audio && ! is _youtube ) {
// tiktok videos fail when using the default format
qualityPath = null ;
} else if ( ! is _audio && ! is _youtube && ( url . includes ( 'reddit' ) || url . includes ( 'pornhub' ) ) ) {
qualityPath = [ '-f' , 'bestvideo+bestaudio' ]
}
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 ) {
customOutput = options . noRelativePath ? customOutput : path . join ( fileFolderPath , customOutput ) ;
downloadConfig = [ '-o' , ` ${ customOutput } .%(ext)s ` , '--write-info-json' , '--print-json' ] ;
} else {
downloadConfig = [ '-o' , path . join ( fileFolderPath , videopath + ( is _audio ? '.%(ext)s' : '.mp4' ) ) , '--write-info-json' , '--print-json' ] ;
}
if ( qualityPath && options . downloading _method === 'exec' ) downloadConfig . push ( ... qualityPath ) ;
if ( is _audio && ! options . skip _audio _args ) {
downloadConfig . push ( '-x' ) ;
downloadConfig . push ( '--audio-format' , 'mp3' ) ;
}
if ( youtubeUsername && youtubePassword ) {
downloadConfig . push ( '--username' , youtubeUsername , '--password' , youtubePassword ) ;
}
if ( useCookies ) {
if ( await fs . pathExists ( path . join ( _ _dirname , 'appdata' , 'cookies.txt' ) ) ) {
downloadConfig . push ( '--cookies' , path . join ( 'appdata' , 'cookies.txt' ) ) ;
} else {
logger . warn ( 'Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.' ) ;
}
}
if ( ! useDefaultDownloadingAgent && customDownloadingAgent ) {
downloadConfig . splice ( 0 , 0 , '--external-downloader' , customDownloadingAgent ) ;
}
let useYoutubeDLArchive = config _api . getConfigItem ( 'ytdl_use_youtubedl_archive' ) ;
if ( useYoutubeDLArchive ) {
const archive _folder = options . user ? path . join ( fileFolderPath , 'archives' ) : archivePath ;
const archive _path = path . join ( archive _folder , ` archive_ ${ type } .txt ` ) ;
await fs . ensureDir ( archive _folder ) ;
// create archive file if it doesn't exist
if ( ! ( await fs . pathExists ( archive _path ) ) ) {
await fs . close ( await fs . open ( 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 ( ! ( await fs . pathExists ( blacklist _path ) ) ) {
await fs . close ( await fs . open ( blacklist _path , 'w' ) ) ;
}
let merged _path = path . join ( fileFolderPath , ` merged_ ${ type } .txt ` ) ;
await fs . ensureFile ( merged _path ) ;
// merges blacklist and regular archive
let inputPathList = [ archive _path , blacklist _path ] ;
let status = await mergeFiles ( inputPathList , merged _path ) ;
options . merged _string = await fs . readFile ( merged _path , "utf8" ) ;
downloadConfig . push ( '--download-archive' , merged _path ) ;
}
if ( config _api . getConfigItem ( 'ytdl_include_thumbnail' ) ) {
downloadConfig . push ( '--write-thumbnail' ) ;
}
if ( globalArgs && globalArgs !== '' ) {
// adds global args
if ( downloadConfig . indexOf ( '-o' ) !== - 1 && globalArgs . split ( ',,' ) . indexOf ( '-o' ) !== - 1 ) {
// if global args has an output, replce the original output with that of global args
const original _output _index = downloadConfig . indexOf ( '-o' ) ;
downloadConfig . splice ( original _output _index , 2 ) ;
}
downloadConfig = downloadConfig . concat ( globalArgs . split ( ',,' ) ) ;
}
}
logger . verbose ( ` youtube-dl args being used: ${ downloadConfig . join ( ',' ) } ` ) ;
return downloadConfig ;
}
async function getVideoInfoByURL ( url , args = [ ] , download = null ) {
return new Promise ( resolve => {
// remove bad args
const new _args = [ ... args ] ;
const archiveArgIndex = new _args . indexOf ( '--download-archive' ) ;
if ( archiveArgIndex !== - 1 ) {
new _args . splice ( archiveArgIndex , 2 ) ;
}
// actually get info
youtubedl . getInfo ( url , new _args , ( err , output ) => {
if ( output ) {
resolve ( output ) ;
} else {
logger . error ( ` Error while retrieving info on video with URL ${ url } with the following message: ${ err } ` ) ;
if ( download ) {
download [ 'error' ] = ` Failed pre-check for video info: ${ err } ` ;
updateDownloads ( ) ;
}
resolve ( null ) ;
}
} ) ;
} ) ;
}
// 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 ) ;
} ) ;
} ) ;
}
async function convertFileToMp3 ( input _file , output _file ) {
logger . verbose ( ` Converting ${ input _file } to ${ output _file } ... ` ) ;
return new Promise ( resolve => {
ffmpeg ( input _file ) . noVideo ( ) . toFormat ( 'mp3' )
. on ( 'end' , ( ) => {
logger . verbose ( ` Conversion for ' ${ output _file } ' complete. ` ) ;
fs . unlinkSync ( input _file )
resolve ( true ) ;
} )
. on ( 'error' , ( err ) => {
logger . error ( 'Failed to convert audio file to the correct format.' ) ;
logger . error ( err ) ;
resolve ( false ) ;
} ) . save ( output _file ) ;
} ) ;
}
async 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 ;
await fs . appendFile ( blacklistPath , line ) ;
}
// download management functions
function updateDownloads ( ) {
db . assign ( { downloads : downloads } ) . write ( ) ;
}
function checkDownloadPercent ( download ) {
/ *
This is more of an art than a science , we ' re just selecting files that start with the file name ,
thus capturing the parts being downloaded in files named like so : '<video title>.<format>.<ext>.part' .
Any file that starts with < video title > will be counted as part of the "bytes downloaded" , which will
be divided by the "total expected bytes."
* /
const file _id = download [ 'file_id' ] ;
const filename = path . format ( path . parse ( download [ '_filename' ] . substring ( 0 , download [ '_filename' ] . length - 4 ) ) ) ;
const resulting _file _size = download [ 'filesize' ] ;
if ( ! resulting _file _size ) return ;
glob ( ` ${ filename } * ` , ( err , files ) => {
let sum _size = 0 ;
files . forEach ( file => {
try {
const file _stats = fs . statSync ( file ) ;
if ( file _stats && file _stats . size ) {
sum _size += file _stats . size ;
}
} catch ( e ) {
}
} ) ;
download [ 'percent_complete' ] = ( sum _size / resulting _file _size * 100 ) . toFixed ( 2 ) ;
updateDownloads ( ) ;
} ) ;
}
// youtube-dl functions
async function startYoutubeDL ( ) {
// auto update youtube-dl
await autoUpdateYoutubeDL ( ) ;
}
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
async function autoUpdateYoutubeDL ( ) {
return new Promise ( async resolve => {
const default _downloader = config _api . getConfigItem ( 'ytdl_default_downloader' ) ;
const using _youtube _dlc = default _downloader === 'youtube-dlc' ;
const youtube _dl _tags _url = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags'
const youtube _dlc _tags _url = 'https://api.github.com/repos/blackjack4494/yt-dlc/tags'
// 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 = using _youtube _dlc ? youtube _dlc _tags _url : youtube _dl _tags _url ;
if ( default _downloader === 'youtube-dl' ) {
await downloadLatestYoutubeDLBinary ( 'unknown' , 'unknown' ) ;
resolve ( true ) ;
return ;
}
fetch ( youtubedl _api _path , { method : 'Get' } )
. then ( async res => res . json ( ) )
. then ( async ( json ) => {
// check if the versions are different
if ( ! json || ! json [ 0 ] ) {
logger . error ( ` Failed to check ${ default _downloader } version for an update. ` )
resolve ( false ) ;
return false ;
}
const latest _update _version = json [ 0 ] [ 'name' ] ;
if ( current _version !== latest _update _version ) {
// versions different, download new update
logger . info ( ` Found new update for ${ default _downloader } . Updating binary... ` ) ;
try {
await checkExistsWithTimeout ( stored _binary _path , 10000 ) ;
} catch ( e ) {
logger . error ( ` Failed to update ${ default _downloader } - ${ e } ` ) ;
}
if ( using _youtube _dlc ) await downloadLatestYoutubeDLCBinary ( latest _update _version ) ;
else await downloadLatestYoutubeDLBinary ( current _version , latest _update _version ) ;
resolve ( true ) ;
} else {
resolve ( false ) ;
}
} )
. catch ( err => {
logger . error ( ` Failed to check ${ default _downloader } version for an update. ` )
logger . error ( err )
} ) ;
} ) ;
}
async function downloadLatestYoutubeDLBinary ( current _version , new _version ) {
return new Promise ( resolve => {
let binary _path = 'node_modules/youtube-dl/bin' ;
downloader ( binary _path , function error ( err , done ) {
if ( err ) {
logger . error ( ` youtube-dl failed to update. Restart the server to try again. ` ) ;
logger . error ( err ) ;
resolve ( false ) ;
}
logger . info ( ` youtube-dl successfully updated! ` ) ;
resolve ( true ) ;
} ) ;
} ) ;
}
async function downloadLatestYoutubeDLCBinary ( new _version ) {
const file _ext = is _windows ? '.exe' : '' ;
const download _url = ` https://github.com/blackjack4494/yt-dlc/releases/latest/download/youtube-dlc ${ file _ext } ` ;
const output _path = ` node_modules/youtube-dl/bin/youtube-dl ${ file _ext } ` ;
await fetchFile ( download _url , output _path , ` youtube-dlc ${ new _version } ` ) ;
const details _path = 'node_modules/youtube-dl/bin/details' ;
const details _json = fs . readJSONSync ( 'node_modules/youtube-dl/bin/details' ) ;
details _json [ 'version' ] = new _version ;
fs . writeJSONSync ( details _path , details _json ) ;
}
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 ( '.' ) ;
}
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/stream/' ) || req . path . includes ( '/api/thumbnail/' ) ) {
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/stream' ) ||
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 playlist _id = using _body ? req . body . id : req . query . id ;
const file = ! playlist _id ? auth _api . getUserVideo ( uuid , uid , type , true , req . body ) : auth _api . getUserPlaylist ( uuid , playlist _id , null , false ) ;
if ( file ) {
req . can _watch = true ;
return next ( ) ;
} else {
res . sendStatus ( 401 ) ;
return ;
}
} else if ( multiUserMode && ! ( req . path . includes ( '/api/auth/register' ) && ! ( req . path . includes ( '/api/config' ) ) && ! 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' , optionalJwt , 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 . 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 safeDownloadOverride = config _api . getConfigItem ( 'ytdl_safe_download_override' ) || config _api . globalArgsRequiresSafeDownload ( ) ;
if ( safeDownloadOverride ) logger . verbose ( 'Download is running with the safe download override.' ) ;
const is _playlist = url . includes ( 'playlist' ) ;
let result _obj = null ;
if ( true || safeDownloadOverride || is _playlist || options . customQualityConfiguration || options . customArgs || options . maxBitrate )
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 ) ;
}
} ) ;
app . post ( '/api/tomp4' , optionalJwt , async function ( req , res ) {
req . setTimeout ( 0 ) ; // remove timeout in case of long videos
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 safeDownloadOverride = config _api . getConfigItem ( 'ytdl_safe_download_override' ) || config _api . globalArgsRequiresSafeDownload ( ) ;
if ( safeDownloadOverride ) logger . verbose ( 'Download is running with the safe download override.' ) ;
const is _playlist = url . includes ( 'playlist' ) ;
let result _obj = null ;
if ( true || safeDownloadOverride || is _playlist || options . customQualityConfiguration || options . customArgs || options . selectedHeight || ! url . includes ( 'youtu' ) )
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 ) ;
}
} ) ;
app . post ( '/api/killAllDownloads' , optionalJwt , async function ( req , res ) {
const result _obj = await killAllDownloads ( ) ;
res . send ( result _obj ) ;
} ) ;
/ * *
* add thumbnails if present
* @ param files - List of files with thumbnailPath property .
* /
async function addThumbnails ( files ) {
await Promise . all ( files . map ( async file => {
const thumbnailPath = file [ 'thumbnailPath' ] ;
if ( thumbnailPath && ( await fs . pathExists ( thumbnailPath ) ) ) {
file [ 'thumbnailBlob' ] = await fs . readFile ( thumbnailPath ) ;
}
} ) ) ;
}
// gets all download mp3s
app . get ( '/api/getMp3s' , optionalJwt , async function ( req , res ) {
var mp3s = db . get ( 'files' ) . chain ( ) . find ( { isAudio : true } ) . 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' ) ;
}
mp3s = JSON . parse ( JSON . stringify ( mp3s ) ) ;
if ( config _api . getConfigItem ( 'ytdl_include_thumbnail' ) ) {
// add thumbnails if present
// await addThumbnails(mp3s);
}
res . send ( {
mp3s : mp3s ,
playlists : playlists
} ) ;
} ) ;
// gets all download mp4s
app . get ( '/api/getMp4s' , optionalJwt , async function ( req , res ) {
var mp4s = db . get ( 'files' ) . chain ( ) . find ( { isAudio : false } ) . value ( ) ; // getMp4s();
var playlists = db . get ( 'playlists' ) . 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' ) ;
}
mp4s = JSON . parse ( JSON . stringify ( mp4s ) ) ;
if ( config _api . getConfigItem ( 'ytdl_include_thumbnail' ) ) {
// add thumbnails if present
// await addThumbnails(mp4s);
}
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 ) ;
} else if ( uuid ) {
file = auth _api . getUserVideo ( uuid , uid , true ) ;
} else {
file = db . get ( 'files' ) . find ( { uid : uid } ) . value ( ) ;
}
// check if chat exists for twitch videos
if ( file && file [ 'url' ] . includes ( 'twitch.tv' ) ) file [ 'chat_exists' ] = fs . existsSync ( file [ 'path' ] . substring ( 0 , file [ 'path' ] . length - 4 ) + '.twitch_chat.json' ) ;
if ( file ) {
res . send ( {
success : true ,
file : file
} ) ;
} else {
res . send ( {
success : false
} ) ;
}
} ) ;
app . post ( '/api/getAllFiles' , optionalJwt , async function ( req , res ) {
// these are returned
let files = null ;
let playlists = null ;
let subscriptions = config _api . getConfigItem ( 'ytdl_allow_subscriptions' ) ? ( subscriptions _api . getAllSubscriptions ( req . isAuthenticated ( ) ? req . user . uid : null ) ) : [ ] ;
// get basic info depending on multi-user mode being enabled
if ( req . isAuthenticated ( ) ) {
files = auth _api . getUserVideos ( req . user . uid ) ;
playlists = auth _api . getUserPlaylists ( req . user . uid ) ;
} else {
files = db . get ( 'files' ) . value ( ) ;
playlists = db . get ( 'playlists' ) . value ( ) ;
}
// loop through subscriptions and add videos
for ( let i = 0 ; i < subscriptions . length ; i ++ ) {
sub = subscriptions [ i ] ;
if ( ! sub . videos ) continue ;
// add sub id for UI
for ( let j = 0 ; j < sub . videos . length ; j ++ ) {
sub . videos [ j ] . sub _id = sub . id ;
}
files = files . concat ( sub . videos ) ;
}
files = JSON . parse ( JSON . stringify ( files ) ) ;
if ( config _api . getConfigItem ( 'ytdl_include_thumbnail' ) ) {
// add thumbnails if present
// await addThumbnails(files);
}
res . send ( {
files : files ,
playlists : playlists
} ) ;
} ) ;
app . post ( '/api/getFullTwitchChat' , optionalJwt , async ( req , res ) => {
var id = req . body . id ;
var type = req . body . type ;
var uuid = req . body . uuid ;
var sub = req . body . sub ;
var user _uid = null ;
if ( req . isAuthenticated ( ) ) user _uid = req . user . uid ;
const chat _file = await twitch _api . getTwitchChatByFileID ( id , type , user _uid , uuid , sub ) ;
res . send ( {
chat : chat _file
} ) ;
} ) ;
app . post ( '/api/downloadTwitchChatByVODID' , optionalJwt , async ( req , res ) => {
var id = req . body . id ;
var type = req . body . type ;
var vodId = req . body . vodId ;
var uuid = req . body . uuid ;
var sub = req . body . sub ;
var user _uid = null ;
if ( req . isAuthenticated ( ) ) user _uid = req . user . uid ;
// check if file already exists. if so, send that instead
const file _exists _check = await twitch _api . getTwitchChatByFileID ( id , type , user _uid , uuid , sub ) ;
if ( file _exists _check ) {
res . send ( { chat : file _exists _check } ) ;
return ;
}
const full _chat = await twitch _api . downloadTwitchChatByVODID ( vodId , id , type , user _uid , sub ) ;
res . send ( {
chat : full _chat
} ) ;
} ) ;
// video sharing
app . post ( '/api/enableSharing' , optionalJwt , function ( req , res ) {
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 , is _playlist , true ) ;
res . send ( { success : success } ) ;
return ;
}
// single-user mode
try {
success = true ;
if ( ! is _playlist && type !== 'subscription' ) {
db . get ( ` files ` )
. find ( { uid : uid } )
. assign ( { sharingEnabled : true } )
. write ( ) ;
} else if ( is _playlist ) {
db . get ( ` playlists ` )
. 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 , is _playlist , false ) ;
res . send ( { success : success } ) ;
return ;
}
// single-user mode
try {
success = true ;
if ( ! is _playlist && type !== 'subscription' ) {
db . get ( ` files ` )
. find ( { uid : uid } )
. assign ( { sharingEnabled : false } )
. write ( ) ;
} else if ( is _playlist ) {
db . get ( ` playlists ` )
. 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/incrementViewCount' , optionalJwt , async ( req , res ) => {
let file _uid = req . body . file _uid ;
let sub _id = req . body . sub _id ;
let uuid = req . body . uuid ;
if ( ! uuid && req . isAuthenticated ( ) ) {
uuid = req . user . uid ;
}
const file _obj = await db _api . getVideo ( file _uid , uuid , sub _id ) ;
const current _view _count = file _obj && file _obj [ 'local_view_count' ] ? file _obj [ 'local_view_count' ] : 0 ;
const new _view _count = current _view _count + 1 ;
await db _api . setVideoProperty ( file _uid , { local _view _count : new _view _count } , uuid , sub _id ) ;
res . send ( {
success : true
} ) ;
} ) ;
// categories
app . post ( '/api/getAllCategories' , optionalJwt , async ( req , res ) => {
const categories = db . get ( 'categories' ) . value ( ) ;
res . send ( { categories : categories } ) ;
} ) ;
app . post ( '/api/createCategory' , optionalJwt , async ( req , res ) => {
const name = req . body . name ;
const new _category = {
name : name ,
uid : uuid ( ) ,
rules : [ ] ,
custom _putput : ''
} ;
db . get ( 'categories' ) . push ( new _category ) . write ( ) ;
res . send ( {
new _category : new _category ,
success : ! ! new _category
} ) ;
} ) ;
app . post ( '/api/deleteCategory' , optionalJwt , async ( req , res ) => {
const category _uid = req . body . category _uid ;
db . get ( 'categories' ) . remove ( { uid : category _uid } ) . write ( ) ;
res . send ( {
success : true
} ) ;
} ) ;
app . post ( '/api/updateCategory' , optionalJwt , async ( req , res ) => {
const category = req . body . category ;
db . get ( 'categories' ) . find ( { uid : category . uid } ) . assign ( category ) . write ( ) ;
res . send ( { success : true } ) ;
} ) ;
app . post ( '/api/updateCategories' , optionalJwt , async ( req , res ) => {
const categories = req . body . categories ;
db . get ( 'categories' ) . assign ( categories ) . write ( ) ;
res . send ( { success : true } ) ;
} ) ;
// subscriptions
app . post ( '/api/subscribe' , optionalJwt , async ( req , res ) => {
let name = req . body . name ;
let url = req . body . url ;
let maxQuality = req . body . maxQuality ;
let timerange = req . body . timerange ;
let streamingOnly = req . body . streamingOnly ;
let audioOnly = req . body . audioOnly ;
let customArgs = req . body . customArgs ;
let customOutput = req . body . customFileOutput ;
let user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const new _sub = {
name : name ,
url : url ,
maxQuality : maxQuality ,
id : uuid ( ) ,
streamingOnly : streamingOnly ,
user _uid : user _uid ,
type : audioOnly ? 'audio' : 'video'
} ;
// adds timerange if it exists, otherwise all videos will be downloaded
if ( timerange ) {
new _sub . timerange = timerange ;
}
if ( customArgs && customArgs !== '' ) {
new _sub . custom _args = customArgs ;
}
if ( customOutput && customOutput !== '' ) {
new _sub . custom _output = customOutput ;
}
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 file _uid = req . body . file _uid ;
let sub = req . body . sub ;
let user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
let success = await subscriptions _api . deleteSubscriptionFile ( sub , file , deleteForever , file _uid , 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 subName = req . body . name ; // if included, subID is optional
let user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
// get sub from db
let subscription = null ;
if ( subID ) {
subscription = subscriptions _api . getSubscription ( subID , user _uid )
} else if ( subName ) {
subscription = subscriptions _api . getSubscriptionByName ( subName , 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 ) {
var parsed _files = subscription . videos ;
if ( ! parsed _files ) {
parsed _files = [ ] ;
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 = await utils . 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 ;
}
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 = utils . 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 utils . File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file , upload _date , jsonobj . description , jsonobj . view _count , jsonobj . height , jsonobj . abr ) ;
parsed _files . push ( file _obj ) ;
}
} else {
// loop through files for extra processing
for ( let i = 0 ; i < parsed _files . length ; i ++ ) {
const file = parsed _files [ i ] ;
// check if chat exists for twitch videos
if ( file && file [ 'url' ] . includes ( 'twitch.tv' ) ) file [ 'chat_exists' ] = fs . existsSync ( file [ 'path' ] . substring ( 0 , file [ 'path' ] . length - 4 ) + '.twitch_chat.json' ) ;
}
}
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 utils . File ( video . title , video . title , video . thumbnail , false , video . duration , video . url , video . uploader , video . size , null , null , video . upload _date , video . view _count , video . height , video . abr ) ) ;
}
}
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/updateSubscription' , optionalJwt , async ( req , res ) => {
let updated _sub = req . body . subscription ;
let user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
let success = subscriptions _api . updateSubscription ( updated _sub , user _uid ) ;
res . send ( {
success : success
} ) ;
} ) ;
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 duration = req . body . duration ;
let new _playlist = {
name : playlistName ,
fileNames : fileNames ,
id : shortid . generate ( ) ,
thumbnailURL : thumbnailURL ,
type : type ,
registered : Date . now ( ) ,
duration : duration
} ;
if ( req . isAuthenticated ( ) ) {
auth _api . addPlaylist ( req . user . uid , new _playlist , type ) ;
} else {
db . get ( ` playlists ` )
. 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 uuid = req . body . uuid ;
let playlist = null ;
if ( req . isAuthenticated ( ) ) {
playlist = auth _api . getUserPlaylist ( uuid ? uuid : req . user . uid , playlistID ) ;
type = playlist . type ;
} else {
playlist = db . get ( ` playlists ` ) . find ( { id : playlistID } ) . value ( ) ;
}
res . send ( {
playlist : playlist ,
type : type ,
success : ! ! playlist
} ) ;
} ) ;
app . post ( '/api/updatePlaylistFiles' , optionalJwt , async ( req , res ) => {
let playlistID = req . body . playlistID ;
let fileNames = req . body . fileNames ;
let success = false ;
try {
if ( req . isAuthenticated ( ) ) {
auth _api . updatePlaylistFiles ( req . user . uid , playlistID , fileNames ) ;
} else {
db . get ( ` playlists ` )
. 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/updatePlaylist' , optionalJwt , async ( req , res ) => {
let playlist = req . body . playlist ;
let success = db _api . updatePlaylist ( playlist , req . user && req . user . uid ) ;
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/deletePlaylist' , optionalJwt , async ( req , res ) => {
let playlistID = req . body . playlistID ;
let success = null ;
try {
if ( req . isAuthenticated ( ) ) {
auth _api . removePlaylist ( req . user . uid , playlistID ) ;
} else {
// removes playlist from playlists
db . get ( ` playlists ` )
. remove ( { id : playlistID } )
. write ( ) ;
}
success = true ;
} catch ( e ) {
success = false ;
}
res . send ( {
success : success
} )
} ) ;
// deletes non-subscription files
app . post ( '/api/deleteFile' , optionalJwt , async ( req , res ) => {
var uid = req . body . uid ;
var type = req . body . type ;
var blacklistMode = req . body . blacklistMode ;
if ( req . isAuthenticated ( ) ) {
let success = await auth _api . deleteUserFile ( req . user . uid , uid , blacklistMode ) ;
res . send ( success ) ;
return ;
}
var file _obj = db . get ( ` files ` ) . find ( { uid : uid } ) . value ( ) ;
var name = file _obj . id ;
var fullpath = file _obj ? file _obj . path : null ;
var wasDeleted = false ;
if ( await fs . pathExists ( fullpath ) )
{
wasDeleted = type === 'audio' ? await deleteAudioFile ( name , path . basename ( fullpath ) , blacklistMode ) : await deleteVideoFile ( name , path . basename ( fullpath ) , blacklistMode ) ;
db . get ( 'files' ) . remove ( { uid : uid } ) . write ( ) ;
wasDeleted = true ;
res . send ( wasDeleted ) ;
} else if ( video _obj ) {
db . get ( 'files' ) . remove ( { uid : uid } ) . write ( ) ;
wasDeleted = true ;
res . send ( wasDeleted ) ;
} else {
wasDeleted = false ;
res . send ( wasDeleted ) ;
}
} ) ;
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 ;
const multiUserMode = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) ;
if ( multiUserMode && ( req . body . uuid || req . user . uid ) ) {
usersFileFolder = config _api . getConfigItem ( 'ytdl_users_base_path' ) ;
base _path = path . join ( usersFileFolder , req . body . uuid ? req . body . uuid : 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 || subscriptionPlaylist === 'true' ? 'playlists' : 'channels' ) , subscriptionName , fileNames + ext ) ;
}
} else {
for ( let i = 0 ; i < fileNames . length ; i ++ ) {
fileNames [ i ] = decodeURIComponent ( fileNames [ i ] ) ;
}
file = await createPlaylistZipFile ( fileNames , type , outputName , fullPathProvided , req . body . uuid ) ;
if ( ! path . isAbsolute ( file ) ) file = path . join ( _ _dirname , file ) ;
}
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/downloadArchive' , async ( req , res ) => {
let sub = req . body . sub ;
let archive _dir = sub . archive ;
let full _archive _path = path . join ( archive _dir , 'archive.txt' ) ;
if ( await fs . pathExists ( full _archive _path ) ) {
res . sendFile ( full _archive _path ) ;
} else {
res . sendStatus ( 404 ) ;
}
} ) ;
var upload _multer = multer ( { dest : _ _dirname + '/appdata/' } ) ;
app . post ( '/api/uploadCookies' , upload _multer . single ( 'cookies' ) , async ( req , res ) => {
const new _path = path . join ( _ _dirname , 'appdata' , 'cookies.txt' ) ;
if ( await fs . pathExists ( req . file . path ) ) {
await fs . rename ( req . file . path , new _path ) ;
} else {
res . sendStatus ( 500 ) ;
return ;
}
if ( await fs . pathExists ( new _path ) ) {
res . send ( { success : true } ) ;
} else {
res . sendStatus ( 500 ) ;
}
} ) ;
// 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
} ) ;
} ) ;
// 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/stream/:id' , optionalJwt , ( req , res ) => {
const type = req . query . type ;
const ext = type === 'audio' ? '.mp3' : '.mp4' ;
const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4' ;
var head ;
let optionalParams = url _api . parse ( req . url , true ) . query ;
let id = decodeURIComponent ( req . params . id ) ;
let file _path = req . query . file _path ? decodeURIComponent ( req . query . file _path . split ( '?' ) [ 0 ] ) : null ;
if ( ! file _path && ( 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 + ext )
} else {
file _path = path . join ( usersFileFolder , req . query . uuid ? req . query . uuid : req . user . uid , type , id + ext ) ;
}
} else if ( ! file _path && 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 + ext ;
}
if ( ! file _path ) {
file _path = path . join ( videoFolderPath , id + ext ) ;
}
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' : mimetype ,
}
res . writeHead ( 206 , head ) ;
file . pipe ( res ) ;
} else {
head = {
'Content-Length' : fileSize ,
'Content-Type' : mimetype ,
}
res . writeHead ( 200 , head )
fs . createReadStream ( file _path ) . pipe ( res )
}
} ) ;
app . get ( '/api/thumbnail/:path' , optionalJwt , async ( req , res ) => {
let file _path = decodeURIComponent ( req . params . path ) ;
if ( fs . existsSync ( file _path ) ) path . isAbsolute ( file _path ) ? res . sendFile ( file _path ) : res . sendFile ( path . join ( _ _dirname , file _path ) ) ;
else res . sendStatus ( 404 ) ;
} ) ;
// 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 } ) ;
} ) ;
// logs management
app . post ( '/api/logs' , async function ( req , res ) {
let logs = null ;
let lines = req . body . lines ;
logs _path = path . join ( 'appdata' , 'logs' , 'combined.log' )
if ( await fs . pathExists ( logs _path ) ) {
if ( lines ) logs = await read _last _lines . read ( logs _path , lines ) ;
else logs = await fs . readFile ( logs _path , 'utf8' ) ;
}
else
logger . error ( ` Failed to find logs file at the expected location: ${ logs _path } ` )
res . send ( {
logs : logs ,
success : ! ! logs
} ) ;
} ) ;
app . post ( '/api/clearAllLogs' , async function ( req , res ) {
logs _path = path . join ( 'appdata' , 'logs' , 'combined.log' ) ;
logs _err _path = path . join ( 'appdata' , 'logs' , 'error.log' ) ;
let success = false ;
try {
await Promise . all ( [
fs . writeFile ( logs _path , '' ) ,
fs . writeFile ( logs _err _path , '' )
] )
success = true ;
} catch ( e ) {
logger . error ( e ) ;
}
res . send ( {
success : success
} ) ;
} ) ;
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' || type === 'video' ) {
result = await getAudioOrVideoInfos ( type , 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' , 'ldapauth' ] , { } )
, 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 . body . 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/updateUser' , 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
await fs . remove ( 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 ) ) ;