const { uuid } = require ( 'uuidv4' ) ;
const fs = require ( 'fs-extra' ) ;
const { promisify } = require ( 'util' ) ;
const auth _api = require ( './authentication/auth' ) ;
const path = require ( 'path' ) ;
const compression = require ( 'compression' ) ;
const multer = require ( 'multer' ) ;
const express = require ( "express" ) ;
const bodyParser = require ( "body-parser" ) ;
const archiver = require ( 'archiver' ) ;
const unzipper = require ( 'unzipper' ) ;
const db _api = require ( './db' ) ;
const utils = require ( './utils' )
const low = require ( 'lowdb' )
const fetch = require ( 'node-fetch' ) ;
const URL = require ( 'url' ) . URL ;
const CONSTS = require ( './consts' )
const read _last _lines = require ( 'read-last-lines' ) ;
const ps = require ( 'ps-node' ) ;
const Feed = require ( 'feed' ) . Feed ;
const session = require ( 'express-session' ) ;
// needed if bin/details somehow gets deleted
if ( ! fs . existsSync ( CONSTS . DETAILS _BIN _PATH ) ) fs . writeJSONSync ( CONSTS . DETAILS _BIN _PATH , { "version" : "2000.06.06" , "path" : "node_modules\\youtube-dl\\bin\\youtube-dl.exe" , "exec" : "youtube-dl.exe" , "downloader" : "youtube-dl" } )
const youtubedl = require ( 'youtube-dl' ) ;
const logger = require ( './logger' ) ;
const config _api = require ( './config.js' ) ;
const downloader _api = require ( './downloader' ) ;
const tasks _api = require ( './tasks' ) ;
const subscriptions _api = require ( './subscriptions' ) ;
const categories _api = require ( './categories' ) ;
const twitch _api = require ( './twitch' ) ;
const youtubedl _api = require ( './youtube-dl' ) ;
const archive _api = require ( './archive' ) ;
const files _api = require ( './files' ) ;
var app = express ( ) ;
// database setup
const FileSync = require ( 'lowdb/adapters/FileSync' ) ;
const adapter = new FileSync ( './appdata/db.json' ) ;
const db = low ( adapter )
const users _adapter = new FileSync ( './appdata/users.json' ) ;
const users _db = low ( users _adapter ) ;
// 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
config _api . initialize ( ) ;
db _api . initialize ( db , users _db ) ;
auth _api . initialize ( db _api ) ;
// Set some defaults
db . defaults (
{
playlists : [ ] ,
files : [ ] ,
configWriteFlag : false ,
downloads : { } ,
subscriptions : [ ] ,
files _to _db _migration _complete : false ,
tasks _manager _role _migration _complete : false ,
archives _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
let url = null ;
let backendPort = null ;
let useDefaultDownloadingAgent = null ;
let customDownloadingAgent = null ;
let allowSubscriptions = null ;
// other needed values
let url _domain = null ;
let updaterStatus = null ;
const concurrentStreams = { } ;
if ( debugMode ) logger . info ( 'YTDL-Material in debug mode!' ) ;
// check if just updated
const just _updated = fs . existsSync ( 'restart_update.json' ) ;
if ( just _updated ) {
updaterStatus = {
updating : false ,
details : 'Update complete! You are now on ' + CONSTS [ 'CURRENT_VERSION' ]
}
fs . unlinkSync ( 'restart_update.json' ) ;
}
if ( fs . existsSync ( 'restart_general.json' ) ) fs . unlinkSync ( 'restart_general.json' ) ;
// updates & starts youtubedl (commented out b/c of repo takedown)
// startYoutubeDL();
var validDownloadingAgents = [
'aria2c' ,
'avconv' ,
'axel' ,
'curl' ,
'ffmpeg' ,
'httpie' ,
'wget'
] ;
const subscription _timeouts = { } ;
let version _info = null ;
if ( fs . existsSync ( 'version.json' ) ) {
version _info = fs . readJSONSync ( 'version.json' ) ;
logger . verbose ( ` Version info: ${ JSON . stringify ( version _info , null , 2 ) } ` ) ;
} else {
version _info = { 'type' : 'N/A' , 'tag' : 'N/A' , 'commit' : 'N/A' , 'date' : 'N/A' } ;
}
// don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
// checks if config exists, if not, a config is auto generated
config _api . configExistsCheck ( ) ;
setAndLoadConfig ( ) ;
app . use ( bodyParser . urlencoded ( { extended : false } ) ) ;
app . use ( bodyParser . json ( ) ) ;
// use passport
app . use ( auth _api . passport . initialize ( ) ) ;
app . use ( session ( { secret : uuid ( ) , resave : true , saveUninitialized : true } ) )
app . use ( auth _api . passport . session ( ) ) ;
// actual functions
async function checkMigrations ( ) {
// 4.1->4.2 migration
const simplified _db _migration _complete = db . get ( 'simplified_db_migration_complete' ) . value ( ) ;
if ( ! simplified _db _migration _complete ) {
logger . info ( 'Beginning migration: 4.1->4.2+' )
let success = await simplifyDBFileStructure ( ) ;
success = success && await files _api . addMetadataPropertyToDB ( 'view_count' ) ;
success = success && await files _api . addMetadataPropertyToDB ( 'description' ) ;
success = success && await files _api . addMetadataPropertyToDB ( 'height' ) ;
success = success && await files _api . addMetadataPropertyToDB ( 'abr' ) ;
// sets migration to complete
db . set ( 'simplified_db_migration_complete' , true ) . write ( ) ;
if ( success ) { logger . info ( '4.1->4.2+ migration complete!' ) ; }
else { logger . error ( 'Migration failed: 4.1->4.2+' ) ; }
}
const new _db _system _migration _complete = db . get ( 'new_db_system_migration_complete' ) . value ( ) ;
if ( ! new _db _system _migration _complete ) {
logger . info ( 'Beginning migration: 4.2->4.3+' )
let success = await db _api . importJSONToDB ( db . value ( ) , users _db . value ( ) ) ;
await tasks _api . setupTasks ( ) ; // necessary as tasks were not properly initialized at first
// sets migration to complete
db . set ( 'new_db_system_migration_complete' , true ) . write ( ) ;
if ( success ) { logger . info ( '4.2->4.3+ migration complete!' ) ; }
else { logger . error ( 'Migration failed: 4.2->4.3+' ) ; }
}
const tasks _manager _role _migration _complete = db . get ( 'tasks_manager_role_migration_complete' ) . value ( ) ;
if ( ! tasks _manager _role _migration _complete ) {
logger . info ( 'Checking if tasks manager role permissions exist for admin user...' ) ;
const success = await auth _api . changeRolePermissions ( 'admin' , 'tasks_manager' , 'yes' ) ;
if ( success ) logger . info ( 'Task manager permissions check complete!' ) ;
else logger . error ( 'Failed to auto add tasks manager permissions to admin role!' ) ;
db . set ( 'tasks_manager_role_migration_complete' , true ) . write ( ) ;
}
const archives _migration _complete = db . get ( 'archives_migration_complete' ) . value ( ) ;
if ( ! archives _migration _complete ) {
logger . info ( 'Checking if archives have been migrated...' ) ;
const imported _archives = await archive _api . importArchives ( ) ;
if ( imported _archives ) logger . info ( 'Archives migration complete!' ) ;
else logger . error ( 'Failed to migrate archives!' ) ;
db . set ( 'archives_migration_complete' , true ) . write ( ) ;
}
return true ;
}
async function simplifyDBFileStructure ( ) {
// back up db files
const old _db _file = fs . readJSONSync ( './appdata/db.json' ) ;
const old _users _db _file = fs . readJSONSync ( './appdata/users.json' ) ;
fs . writeJSONSync ( 'appdata/db.old.json' , old _db _file ) ;
fs . writeJSONSync ( 'appdata/users.old.json' , old _users _db _file ) ;
// simplify
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' ) . value ( ) ) ;
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' ) . value ( ) ) ;
db . assign ( { playlists : playlists } ) . write ( ) ;
}
return true ;
}
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 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...'
}
utils . restartServer ( true ) ;
} , err => {
logger . error ( 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 is _dir = fileName . substring ( fileName . length - 1 , fileName . length ) === '/'
if ( ! is _dir && fileName . includes ( 'youtubedl-material/public/' ) ) {
// get public folder files
const 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
const 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 ) ;
} ) ;
} ) ;
}
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 utils . 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 utils . 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 ) ;
let resultList = null ;
try {
resultList = 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 utils . wait ( 100 ) ;
return true ;
}
async function setAndLoadConfig ( ) {
await setConfigFromEnv ( ) ;
await loadConfig ( ) ;
}
async function setConfigFromEnv ( ) {
const config _items = getEnvConfigItems ( ) ;
if ( ! config _items || config _items . length === 0 ) return true ;
const success = config _api . setConfigItems ( config _items ) ;
if ( success ) {
logger . info ( 'Config items set using ENV variables.' ) ;
await utils . wait ( 100 ) ;
return true ;
} else {
logger . error ( 'ERROR: Failed to set config items using ENV variables.' ) ;
return false ;
}
}
async function loadConfig ( ) {
loadConfigValues ( ) ;
// connect to DB
if ( ! config _api . getConfigItem ( 'ytdl_use_local_db' ) )
await db _api . connectToDB ( ) ;
db _api . database _initialized = true ;
db _api . database _initialized _bs . next ( true ) ;
// check migrations
await checkMigrations ( ) ;
// now this is done here due to youtube-dl's repo takedown
await startYoutubeDL ( ) ;
// get subscriptions
if ( allowSubscriptions ) {
// set downloading to false
let subscriptions = await subscriptions _api . getAllSubscriptions ( ) ;
subscriptions . forEach ( async sub => subscriptions _api . writeSubscriptionMetadata ( sub ) ) ;
subscriptions _api . updateSubscriptionPropertyMultiple ( subscriptions , { downloading : false } ) ;
// runs initially, then runs every ${subscriptionCheckInterval} seconds
const watchSubscriptionsInterval = function ( ) {
watchSubscriptions ( ) ;
const subscriptionsCheckInterval = config _api . getConfigItem ( 'ytdl_subscriptions_check_interval' ) ;
setTimeout ( watchSubscriptionsInterval , subscriptionsCheckInterval * 1000 ) ;
}
watchSubscriptionsInterval ( ) ;
}
// 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' ) ;
useDefaultDownloadingAgent = config _api . getConfigItem ( 'ytdl_use_default_downloading_agent' ) ;
customDownloadingAgent = config _api . getConfigItem ( 'ytdl_custom_downloading_agent' ) ;
allowSubscriptions = config _api . getConfigItem ( 'ytdl_allow_subscriptions' ) ;
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' ) ;
utils . updateLoggerLevel ( logger _level ) ;
}
function calculateSubcriptionRetrievalDelay ( subscriptions _amount ) {
// frequency is once every 5 mins by default
const subscriptionsCheckInterval = config _api . getConfigItem ( 'ytdl_subscriptions_check_interval' ) ;
let interval _in _ms = subscriptionsCheckInterval * 1000 ;
const subinterval _in _ms = interval _in _ms / subscriptions _amount ;
return subinterval _in _ms ;
}
async function watchSubscriptions ( ) {
let subscriptions = await subscriptions _api . getAllSubscriptions ( ) ;
if ( ! subscriptions ) return ;
// auto pause deprecated streamingOnly mode
const streaming _only _subs = subscriptions . filter ( sub => sub . streamingOnly ) ;
subscriptions _api . updateSubscriptionPropertyMultiple ( streaming _only _subs , { paused : true } ) ;
const valid _subscriptions = subscriptions . filter ( sub => ! sub . paused && ! sub . streamingOnly ) ;
let subscriptions _amount = valid _subscriptions . length ;
let delay _interval = calculateSubcriptionRetrievalDelay ( subscriptions _amount ) ;
let current _delay = 0 ;
const multiUserMode = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) ;
for ( let i = 0 ; i < valid _subscriptions . length ; i ++ ) {
let sub = valid _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 ;
const subscriptionsCheckInterval = config _api . getConfigItem ( 'ytdl_subscriptions_check_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 ] } ;
}
// currently only works for single urls
async function getUrlInfos ( url ) {
let startDate = Date . now ( ) ;
let result = [ ] ;
return new Promise ( resolve => {
youtubedl . exec ( url , [ '--dump-json' ] , { maxBuffer : Infinity } , ( 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 retrieving formats for ${ url } : ${ err } ` ) ;
resolve ( null ) ;
}
let try _putput = null ;
try {
try _putput = JSON . parse ( output ) ;
result = try _putput ;
} catch ( e ) {
logger . error ( ` Failed to retrieve available formats for url: ${ url } ` ) ;
}
resolve ( result ) ;
} ) ;
} ) ;
}
// youtube-dl functions
async function startYoutubeDL ( ) {
// auto update youtube-dl
youtubedl _api . verifyBinaryExistsLinux ( ) ;
const update _available = await youtubedl _api . checkForYoutubeDLUpdate ( ) ;
if ( update _available ) await youtubedl _api . updateYoutubeDL ( update _available ) ;
}
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/' ) || req . path . includes ( '/api/rss' ) ) {
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 = async 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/getPlaylist' ) ||
req . path . includes ( '/api/downloadFileFromServer' ) ) ) {
// 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 playlist _id = using _body ? req . body . playlist _id : req . query . playlist _id ;
const file = ! playlist _id ? auth _api . getUserVideo ( uuid , uid , true ) : await files _api . getPlaylist ( playlist _id , uuid , true ) ;
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 . get ( '/api/versionInfo' , ( req , res ) => {
res . send ( { version _info : version _info } ) ;
} ) ;
app . post ( '/api/restartServer' , optionalJwt , ( req , res ) => {
// delayed by a little bit so that the client gets a response
setTimeout ( ( ) => { utils . restartServer ( ) } , 100 ) ;
res . send ( { success : true } ) ;
} ) ;
app . get ( '/api/getDBInfo' , optionalJwt , async ( req , res ) => {
const db _info = await db _api . getDBStats ( ) ;
res . send ( db _info ) ;
} ) ;
app . post ( '/api/transferDB' , optionalJwt , async ( req , res ) => {
const local _to _remote = req . body . local _to _remote ;
let success = null ;
let error = '' ;
if ( local _to _remote === config _api . getConfigItem ( 'ytdl_use_local_db' ) ) {
success = await db _api . transferDB ( local _to _remote ) ;
if ( ! success ) error = 'Unknown error' ;
else config _api . setConfigItem ( 'ytdl_use_local_db' , ! local _to _remote ) ;
} else {
success = false ;
error = ` Failed to transfer DB as it cannot transition into its current status: ${ local _to _remote ? 'MongoDB' : 'Local DB' } ` ;
logger . error ( error ) ;
}
res . send ( { success : success , error : error } ) ;
} ) ;
app . post ( '/api/testConnectionString' , optionalJwt , async ( req , res ) => {
const connection _string = req . body . connection _string ;
let success = null ;
let error = '' ;
success = await db _api . connectToDB ( 0 , true , connection _string ) ;
if ( ! success ) error = 'Connection string failed.' ;
res . send ( { success : success , error : error } ) ;
} ) ;
app . post ( '/api/downloadFile' , optionalJwt , async function ( req , res ) {
req . setTimeout ( 0 ) ; // remove timeout in case of long videos
const url = req . body . url ;
const type = req . body . type ? req . body . type : 'video' ;
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const options = {
customArgs : req . body . customArgs ,
additionalArgs : req . body . additionalArgs ,
customOutput : req . body . customOutput ,
selectedHeight : req . body . selectedHeight ,
maxHeight : req . body . maxHeight ,
customQualityConfiguration : req . body . customQualityConfiguration ,
youtubeUsername : req . body . youtubeUsername ,
youtubePassword : req . body . youtubePassword ,
ui _uid : req . body . ui _uid ,
cropFileSettings : req . body . cropFileSettings ,
ignoreArchive : req . body . ignoreArchive
} ;
const download = await downloader _api . createDownload ( url , type , options , user _uid ) ;
if ( download ) {
res . send ( { download : download } ) ;
} else {
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/api/killAllDownloads' , optionalJwt , async function ( req , res ) {
const result _obj = await killAllDownloads ( ) ;
res . send ( result _obj ) ;
} ) ;
app . post ( '/api/generateArgs' , optionalJwt , async function ( req , res ) {
const url = req . body . url ;
const type = req . body . type ;
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const options = {
customArgs : req . body . customArgs ,
additionalArgs : req . body . additionalArgs ,
customOutput : req . body . customOutput ,
selectedHeight : req . body . selectedHeight ,
maxHeight : req . body . maxHeight ,
customQualityConfiguration : req . body . customQualityConfiguration ,
youtubeUsername : req . body . youtubeUsername ,
youtubePassword : req . body . youtubePassword ,
ui _uid : req . body . ui _uid ,
cropFileSettings : req . body . cropFileSettings
} ;
const args = await downloader _api . generateArgs ( url , type , options , user _uid , true ) ;
res . send ( { args : args } ) ;
} ) ;
// gets all download mp3s
app . get ( '/api/getMp3s' , optionalJwt , async function ( req , res ) {
// TODO: simplify
let mp3s = await db _api . getRecords ( 'files' , { isAudio : true } ) ;
let playlists = await db _api . getRecords ( 'playlists' ) ;
const is _authenticated = req . isAuthenticated ( ) ;
if ( is _authenticated ) {
// get user audio files/playlists
auth _api . passport . authenticate ( 'jwt' )
mp3s = await db _api . getRecords ( 'files' , { user _uid : req . user . uid , isAudio : true } ) ;
playlists = await db _api . getRecords ( 'playlists' , { user _uid : req . user . uid } ) ; // TODO: remove?
}
mp3s = JSON . parse ( JSON . stringify ( mp3s ) ) ;
res . send ( {
mp3s : mp3s ,
playlists : playlists
} ) ;
} ) ;
// gets all download mp4s
app . get ( '/api/getMp4s' , optionalJwt , async function ( req , res ) {
let mp4s = await db _api . getRecords ( 'files' , { isAudio : false } ) ;
let playlists = await db _api . getRecords ( 'playlists' ) ;
const is _authenticated = req . isAuthenticated ( ) ;
if ( is _authenticated ) {
// get user videos/playlists
auth _api . passport . authenticate ( 'jwt' )
mp4s = await db _api . getRecords ( 'files' , { user _uid : req . user . uid , isAudio : false } ) ;
playlists = await db _api . getRecords ( 'playlists' , { user _uid : req . user . uid } ) ; // TODO: remove?
}
mp4s = JSON . parse ( JSON . stringify ( mp4s ) ) ;
res . send ( {
mp4s : mp4s ,
playlists : playlists
} ) ;
} ) ;
app . post ( '/api/getFile' , optionalJwt , async function ( req , res ) {
const uid = req . body . uid ;
const uuid = req . body . uuid ;
let file = await db _api . getRecord ( 'files' , { uid : uid } ) ;
if ( uuid && ! file [ 'sharingEnabled' ] ) file = null ;
// 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
const sort = req . body . sort ;
const range = req . body . range ;
const text _search = req . body . text _search ;
const file _type _filter = req . body . file _type _filter ;
const favorite _filter = req . body . favorite _filter ;
const sub _id = req . body . sub _id ;
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const { files , file _count } = await files _api . getAllFiles ( sort , range , text _search , file _type _filter , favorite _filter , sub _id , uuid ) ;
res . send ( {
files : files ,
file _count : file _count ,
} ) ;
} ) ;
app . post ( '/api/updateFile' , optionalJwt , async function ( req , res ) {
const uid = req . body . uid ;
const change _obj = req . body . change _obj ;
const file = await db _api . updateRecord ( 'files' , { uid : uid } , change _obj ) ;
if ( ! file ) {
res . send ( {
success : false ,
error : 'File could not be found'
} ) ;
} else {
res . send ( {
success : true
} ) ;
}
} ) ;
app . post ( '/api/checkConcurrentStream' , async ( req , res ) => {
const uid = req . body . uid ;
const DEAD _SERVER _THRESHOLD = 10 ;
if ( concurrentStreams [ uid ] && Date . now ( ) / 1000 - concurrentStreams [ uid ] [ 'unix_timestamp' ] > DEAD _SERVER _THRESHOLD ) {
logger . verbose ( ` Killing dead stream on ${ uid } ` ) ;
delete concurrentStreams [ uid ] ;
}
res . send ( { stream : concurrentStreams [ uid ] } )
} ) ;
app . post ( '/api/updateConcurrentStream' , optionalJwt , async ( req , res ) => {
const uid = req . body . uid ;
const playback _timestamp = req . body . playback _timestamp ;
const unix _timestamp = req . body . unix _timestamp ;
const playing = req . body . playing ;
concurrentStreams [ uid ] = {
playback _timestamp : playback _timestamp ,
unix _timestamp : unix _timestamp ,
playing : playing
}
res . send ( { stream : concurrentStreams [ uid ] } )
} ) ;
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 , async ( 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 ) {
await db _api . updateRecord ( 'files' , { uid : uid } , { sharingEnabled : true } )
} else if ( is _playlist ) {
await db _api . updateRecord ( ` playlists ` , { id : uid } , { sharingEnabled : true } ) ;
} else if ( false ) {
// TODO: Implement.
} else {
// error
success = false ;
}
} catch ( err ) {
logger . error ( err ) ;
success = false ;
}
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/disableSharing' , optionalJwt , async function ( req , res ) {
var type = req . body . type ;
var uid = req . body . uid ;
var is _playlist = req . body . is _playlist ;
let success = null ;
try {
success = true ;
if ( ! is _playlist && type !== 'subscription' ) {
await db _api . updateRecord ( 'files' , { uid : uid } , { sharingEnabled : false } )
} else if ( is _playlist ) {
await db _api . updateRecord ( ` playlists ` , { id : uid } , { sharingEnabled : false } ) ;
} else {
// error
success = false ;
}
} catch ( err ) {
success = false ;
}
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/incrementViewCount' , 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 files _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 = await db _api . getRecords ( 'categories' ) ;
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 _output : ''
} ;
await db _api . insertRecordIntoTable ( 'categories' , new _category ) ;
res . send ( {
new _category : new _category ,
success : ! ! new _category
} ) ;
} ) ;
app . post ( '/api/deleteCategory' , optionalJwt , async ( req , res ) => {
const category _uid = req . body . category _uid ;
await db _api . removeRecord ( 'categories' , { uid : category _uid } ) ;
res . send ( {
success : true
} ) ;
} ) ;
app . post ( '/api/updateCategory' , optionalJwt , async ( req , res ) => {
const category = req . body . category ;
await db _api . updateRecord ( 'categories' , { uid : category . uid } , category )
res . send ( { success : true } ) ;
} ) ;
app . post ( '/api/updateCategories' , optionalJwt , async ( req , res ) => {
const categories = req . body . categories ;
await db _api . removeAllRecords ( 'categories' ) ;
await db _api . insertRecordsIntoTable ( 'categories' , categories ) ;
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 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 ( ) ,
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 _uid = req . body . file _uid ;
let success = await files _api . deleteFile ( file _uid , deleteForever ) ;
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
// get sub from db
let subscription = null ;
if ( subID ) {
subscription = await subscriptions _api . getSubscription ( subID )
} else if ( subName ) {
subscription = await subscriptions _api . getSubscriptionByName ( subName )
}
if ( ! subscription ) {
// failed to get subscription from db, send 400 error
res . sendStatus ( 400 ) ;
return ;
}
subscription = JSON . parse ( JSON . stringify ( subscription ) ) ;
// get sub videos
if ( subscription . name ) {
var parsed _files = await db _api . getRecords ( 'files' , { sub _id : subscription . id } ) ; // subscription.videos;
subscription [ 'videos' ] = parsed _files ;
// 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 {
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/getSubscriptions' , optionalJwt , async ( req , res ) => {
let user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
// get subs from api
let subscriptions = await subscriptions _api . getSubscriptions ( user _uid ) ;
res . send ( {
subscriptions : subscriptions
} ) ;
} ) ;
app . post ( '/api/createPlaylist' , optionalJwt , async ( req , res ) => {
let playlistName = req . body . playlistName ;
let uids = req . body . uids ;
const new _playlist = await files _api . createPlaylist ( playlistName , uids , req . isAuthenticated ( ) ? req . user . uid : null ) ;
res . send ( {
new _playlist : new _playlist ,
success : ! ! new _playlist // always going to be true
} )
} ) ;
app . post ( '/api/getPlaylist' , optionalJwt , async ( req , res ) => {
let playlist _id = req . body . playlist _id ;
let uuid = req . body . uuid ? req . body . uuid : ( req . user && req . user . uid ? req . user . uid : null ) ;
let include _file _metadata = req . body . include _file _metadata ;
const playlist = await files _api . getPlaylist ( playlist _id , uuid ) ;
const file _objs = [ ] ;
if ( playlist && include _file _metadata ) {
for ( let i = 0 ; i < playlist [ 'uids' ] . length ; i ++ ) {
const uid = playlist [ 'uids' ] [ i ] ;
const file _obj = await files _api . getVideo ( uid , uuid ) ;
if ( file _obj ) file _objs . push ( file _obj ) ;
// TODO: remove file from playlist if could not be found
}
}
res . send ( {
playlist : playlist ,
file _objs : file _objs ,
success : ! ! playlist
} ) ;
} ) ;
app . post ( '/api/getPlaylists' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const include _categories = req . body . include _categories ;
let playlists = await db _api . getRecords ( 'playlists' , { user _uid : uuid } ) ;
if ( include _categories ) {
const categories = await categories _api . getCategoriesAsPlaylists ( ) ;
if ( categories ) {
playlists = playlists . concat ( categories ) ;
}
}
res . send ( {
playlists : playlists
} ) ;
} ) ;
app . post ( '/api/addFileToPlaylist' , optionalJwt , async ( req , res ) => {
let playlist _id = req . body . playlist _id ;
let file _uid = req . body . file _uid ;
const playlist = await db _api . getRecord ( 'playlists' , { id : playlist _id } ) ;
playlist . uids . push ( file _uid ) ;
let success = await files _api . updatePlaylist ( playlist ) ;
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/updatePlaylist' , optionalJwt , async ( req , res ) => {
let playlist = req . body . playlist ;
let success = await files _api . updatePlaylist ( playlist , req . user && req . user . uid ) ;
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/deletePlaylist' , optionalJwt , async ( req , res ) => {
let playlistID = req . body . playlist _id ;
let success = null ;
try {
// removes playlist from playlists
await db _api . removeRecord ( 'playlists' , { id : playlistID } )
success = true ;
} catch ( e ) {
success = false ;
}
res . send ( {
success : success
} )
} ) ;
// deletes non-subscription files
app . post ( '/api/deleteFile' , optionalJwt , async ( req , res ) => {
const uid = req . body . uid ;
const blacklistMode = req . body . blacklistMode ;
let wasDeleted = false ;
wasDeleted = await files _api . deleteFile ( uid , blacklistMode ) ;
res . send ( wasDeleted ) ;
} ) ;
app . post ( '/api/deleteAllFiles' , optionalJwt , async ( req , res ) => {
const blacklistMode = false ;
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
let files = null ;
let text _search = req . body . text _search ;
let file _type _filter = req . body . file _type _filter ;
const filter _obj = { user _uid : uuid } ;
const regex = true ;
if ( text _search ) {
if ( regex ) {
filter _obj [ 'title' ] = { $regex : ` .* ${ text _search } .* ` , $options : 'i' } ;
} else {
filter _obj [ '$text' ] = { $search : utils . createEdgeNGrams ( text _search ) } ;
}
}
if ( file _type _filter === 'audio_only' ) filter _obj [ 'isAudio' ] = true ;
else if ( file _type _filter === 'video_only' ) filter _obj [ 'isAudio' ] = false ;
files = await db _api . getRecords ( 'files' , filter _obj ) ;
let file _count = await db _api . getRecords ( 'files' , filter _obj , true ) ;
let delete _count = 0 ;
for ( let i = 0 ; i < files . length ; i ++ ) {
let wasDeleted = false ;
wasDeleted = await files _api . deleteFile ( files [ i ] . uid , blacklistMode ) ;
if ( wasDeleted ) {
delete _count ++ ;
}
}
res . send ( {
file _count : file _count ,
delete _count : delete _count
} ) ;
} ) ;
app . post ( '/api/downloadFileFromServer' , optionalJwt , async ( req , res ) => {
let uid = req . body . uid ;
let uuid = req . body . uuid ;
let playlist _id = req . body . playlist _id ;
let sub _id = req . body . sub _id ;
let file _path _to _download = null ;
if ( ! uuid && req . user ) uuid = req . user . uid ;
let zip _file _generated = false ;
if ( playlist _id ) {
zip _file _generated = true ;
const playlist _files _to _download = [ ] ;
const playlist = await files _api . getPlaylist ( playlist _id , uuid ) ;
for ( let i = 0 ; i < playlist [ 'uids' ] . length ; i ++ ) {
const playlist _file _uid = playlist [ 'uids' ] [ i ] ;
const file _obj = await files _api . getVideo ( playlist _file _uid , uuid ) ;
playlist _files _to _download . push ( file _obj ) ;
}
// generate zip
file _path _to _download = await utils . createContainerZipFile ( playlist [ 'name' ] , playlist _files _to _download ) ;
} else if ( sub _id && ! uid ) {
zip _file _generated = true ;
const sub = await db _api . getRecord ( 'subscriptions' , { id : sub _id } ) ;
const sub _files _to _download = await db _api . getRecords ( 'files' , { sub _id : sub _id } ) ;
// generate zip
file _path _to _download = await utils . createContainerZipFile ( sub [ 'name' ] , sub _files _to _download ) ;
} else {
const file _obj = await files _api . getVideo ( uid , uuid , sub _id )
file _path _to _download = file _obj . path ;
}
if ( ! path . isAbsolute ( file _path _to _download ) ) file _path _to _download = path . join ( _ _dirname , file _path _to _download ) ;
res . sendFile ( file _path _to _download , function ( err ) {
if ( err ) {
logger . error ( err ) ;
} else if ( zip _file _generated ) {
try {
// delete generated zip file
fs . unlinkSync ( file _path _to _download ) ;
} catch ( e ) {
logger . error ( ` Failed to remove file after sending to client: ${ file _path _to _download } ` ) ;
}
}
} ) ;
} ) ;
app . post ( '/api/getArchives' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const sub _id = req . body . sub _id ;
const filter _obj = { user _uid : uuid , sub _id : sub _id } ;
const type = req . body . type ;
// we do this for file types because if type is null, that means get files of all types
if ( type ) filter _obj [ 'type' ] = type ;
const archives = await db _api . getRecords ( 'archives' , filter _obj ) ;
res . send ( {
archives : archives
} ) ;
} ) ;
app . post ( '/api/downloadArchive' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const sub _id = req . body . sub _id ;
const type = req . body . type ;
const archive _text = await archive _api . generateArchive ( type , uuid , sub _id ) ;
if ( archive _text !== null && archive _text !== undefined ) {
res . setHeader ( 'Content-type' , "application/octet-stream" ) ;
res . setHeader ( 'Content-disposition' , 'attachment; filename=archive.txt' ) ;
res . send ( archive _text ) ;
} else {
res . sendStatus ( 400 ) ;
}
} ) ;
app . post ( '/api/importArchive' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const archive = req . body . archive ;
const sub _id = req . body . sub _id ;
const type = req . body . type ;
const archive _text = Buffer . from ( archive . split ( ',' ) [ 1 ] , 'base64' ) . toString ( ) ;
const imported _count = await archive _api . importArchiveFile ( archive _text , type , uuid , sub _id ) ;
res . send ( {
success : ! ! imported _count ,
imported _count : imported _count
} ) ;
} ) ;
app . post ( '/api/deleteArchiveItems' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const archives = req . body . archives ;
let success = true ;
for ( const archive of archives ) {
success &= await archive _api . removeFromArchive ( archive [ 'extractor' ] , archive [ 'id' ] , archive [ 'type' ] , uuid , archive [ 'sub_id' ] ) ;
}
res . send ( {
success : success
} ) ;
} ) ;
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' , optionalJwt , async ( req , res ) => {
let status = updaterStatus ;
if ( status ) {
res . send ( updaterStatus ) ;
} else {
res . sendStatus ( 404 ) ;
}
} ) ;
app . post ( '/api/updateServer' , optionalJwt , async ( req , res ) => {
let tag = req . body . tag ;
updateServer ( tag ) ;
res . send ( {
success : true
} ) ;
} ) ;
// API Key API calls
app . post ( '/api/generateNewAPIKey' , optionalJwt , 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' , optionalJwt , async ( req , res ) => {
const type = req . query . type ;
const uuid = req . query . uuid ? req . query . uuid : ( req . user ? req . user . uid : null ) ;
const sub _id = req . query . sub _id ;
const mimetype = type === 'audio' ? 'audio/mp3' : 'video/mp4' ;
var head ;
let uid = decodeURIComponent ( req . query . uid ) ;
let file _path = null ;
let file _obj = null ;
const multiUserMode = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) ;
if ( ! multiUserMode || req . isAuthenticated ( ) || req . can _watch ) {
file _obj = await files _api . getVideo ( uid , uuid , sub _id ) ;
if ( file _obj ) file _path = file _obj [ 'path' ] ;
else file _path = null ;
}
if ( ! fs . existsSync ( file _path ) ) {
logger . error ( ` File ${ file _path } could not be found! UID: ${ uid } , ID: ${ file _obj && file _obj . id } ` ) ;
}
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 [ uid ] ) config _api . descriptors [ uid ] . push ( file ) ;
else config _api . descriptors [ uid ] = [ file ] ;
file . on ( 'close' , function ( ) {
let index = config _api . descriptors [ uid ] . indexOf ( file ) ;
config _api . descriptors [ uid ] . 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 . post ( '/api/downloads' , optionalJwt , async ( req , res ) => {
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const uids = req . body . uids ;
let downloads = await db _api . getRecords ( 'download_queue' , { user _uid : user _uid } ) ;
if ( uids ) downloads = downloads . filter ( download => uids . includes ( download [ 'uid' ] ) ) ;
res . send ( { downloads : downloads } ) ;
} ) ;
app . post ( '/api/download' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const download = await db _api . getRecord ( 'download_queue' , { uid : download _uid } ) ;
if ( download ) {
res . send ( { download : download } ) ;
} else {
res . send ( { download : null } ) ;
}
} ) ;
app . post ( '/api/clearDownloads' , optionalJwt , async ( req , res ) => {
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const clear _finished = req . body . clear _finished ;
const clear _paused = req . body . clear _paused ;
const clear _errors = req . body . clear _errors ;
let success = true ;
if ( clear _finished ) success &= await db _api . removeAllRecords ( 'download_queue' , { finished : true , user _uid : user _uid } ) ;
if ( clear _paused ) success &= await db _api . removeAllRecords ( 'download_queue' , { paused : true , user _uid : user _uid } ) ;
if ( clear _errors ) success &= await db _api . removeAllRecords ( 'download_queue' , { error : { $ne : null } , user _uid : user _uid } ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/clearDownload' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const success = await downloader _api . clearDownload ( download _uid ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/pauseDownload' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const success = await downloader _api . pauseDownload ( download _uid ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/pauseAllDownloads' , optionalJwt , async ( req , res ) => {
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
let success = true ;
const all _running _downloads = await db _api . getRecords ( 'download_queue' , { paused : false , finished : false , user _uid : user _uid } ) ;
for ( let i = 0 ; i < all _running _downloads . length ; i ++ ) {
success &= await downloader _api . pauseDownload ( all _running _downloads [ i ] [ 'uid' ] ) ;
}
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/resumeDownload' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const success = await downloader _api . resumeDownload ( download _uid ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/resumeAllDownloads' , optionalJwt , async ( req , res ) => {
const user _uid = req . isAuthenticated ( ) ? req . user . uid : null ;
let success = true ;
const all _paused _downloads = await db _api . getRecords ( 'download_queue' , { paused : true , user _uid : user _uid , error : null } ) ;
for ( let i = 0 ; i < all _paused _downloads . length ; i ++ ) {
success &= await downloader _api . resumeDownload ( all _paused _downloads [ i ] [ 'uid' ] ) ;
}
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/restartDownload' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const new _download = await downloader _api . restartDownload ( download _uid ) ;
res . send ( { success : ! ! new _download , new _download _uid : new _download ? new _download [ 'uid' ] : null } ) ;
} ) ;
app . post ( '/api/cancelDownload' , optionalJwt , async ( req , res ) => {
const download _uid = req . body . download _uid ;
const success = await downloader _api . cancelDownload ( download _uid ) ;
res . send ( { success : success } ) ;
} ) ;
// tasks
app . post ( '/api/getTasks' , optionalJwt , async ( req , res ) => {
const tasks = await db _api . getRecords ( 'tasks' ) ;
for ( let task of tasks ) {
if ( task [ 'schedule' ] ) task [ 'next_invocation' ] = tasks _api . TASKS [ task [ 'key' ] ] [ 'job' ] . nextInvocation ( ) . getTime ( ) ;
}
res . send ( { tasks : tasks } ) ;
} ) ;
app . post ( '/api/resetTasks' , optionalJwt , async ( req , res ) => {
const tasks _keys = Object . keys ( tasks _api . TASKS ) ;
for ( let i = 0 ; i < tasks _keys . length ; i ++ ) {
const task _key = tasks _keys [ i ] ;
tasks _api . TASKS [ task _key ] [ 'job' ] = null ;
}
await db _api . removeAllRecords ( 'tasks' ) ;
await tasks _api . setupTasks ( ) ;
res . send ( { success : true } ) ;
} ) ;
app . post ( '/api/getTask' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const task = await db _api . getRecord ( 'tasks' , { key : task _key } ) ;
if ( task [ 'schedule' ] ) task [ 'next_invocation' ] = tasks _api . TASKS [ task _key ] [ 'job' ] . nextInvocation ( ) . getTime ( ) ;
res . send ( { task : task } ) ;
} ) ;
app . post ( '/api/runTask' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const task = await db _api . getRecord ( 'tasks' , { key : task _key } ) ;
let success = true ;
if ( task [ 'running' ] || task [ 'confirming' ] ) success = false ;
else await tasks _api . executeRun ( task _key ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/confirmTask' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const task = await db _api . getRecord ( 'tasks' , { key : task _key } ) ;
let success = true ;
if ( task [ 'running' ] || task [ 'confirming' ] || ! task [ 'data' ] ) success = false ;
else await tasks _api . executeConfirm ( task _key ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/updateTaskSchedule' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const new _schedule = req . body . new _schedule ;
await tasks _api . updateTaskSchedule ( task _key , new _schedule ) ;
res . send ( { success : true } ) ;
} ) ;
app . post ( '/api/updateTaskData' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const new _data = req . body . new _data ;
const success = await db _api . updateRecord ( 'tasks' , { key : task _key } , { data : new _data } ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/updateTaskOptions' , optionalJwt , async ( req , res ) => {
const task _key = req . body . task _key ;
const new _options = req . body . new _options ;
const success = await db _api . updateRecord ( 'tasks' , { key : task _key } , { options : new _options } ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/getDBBackups' , optionalJwt , async ( req , res ) => {
const backup _dir = path . join ( 'appdata' , 'db_backup' ) ;
fs . ensureDirSync ( backup _dir ) ;
const db _backups = [ ] ;
const candidate _backups = await utils . recFindByExt ( backup _dir , 'bak' , null , [ ] , false ) ;
for ( let i = 0 ; i < candidate _backups . length ; i ++ ) {
const candidate _backup = candidate _backups [ i ] ;
// must have specific format
if ( candidate _backup . split ( '.' ) . length - 1 !== 4 ) continue ;
const candidate _backup _path = candidate _backup ;
const stats = fs . statSync ( candidate _backup _path ) ;
db _backups . push ( { name : path . basename ( candidate _backup ) , timestamp : parseInt ( candidate _backup . split ( '.' ) [ 2 ] ) , size : stats . size , source : candidate _backup . includes ( 'local' ) ? 'local' : 'remote' } ) ;
}
db _backups . sort ( ( a , b ) => b . timestamp - a . timestamp ) ;
res . send ( { db _backups : db _backups } ) ;
} ) ;
app . post ( '/api/restoreDBBackup' , optionalJwt , async ( req , res ) => {
const file _name = req . body . file _name ;
const success = await db _api . restoreDB ( file _name ) ;
res . send ( { success : success } ) ;
} ) ;
// logs management
app . post ( '/api/logs' , optionalJwt , async function ( req , res ) {
let logs = null ;
let lines = req . body . lines ;
const 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' , optionalJwt , async function ( req , res ) {
const logs _path = path . join ( 'appdata' , 'logs' , 'combined.log' ) ;
const 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/getFileFormats' , optionalJwt , async ( req , res ) => {
let url = req . body . url ;
let result = await getUrlInfos ( url ) ;
res . send ( {
result : result ,
success : ! ! result
} )
} ) ;
// user authentication
app . post ( '/api/auth/register' , optionalJwt , async ( req , res ) => {
const userid = req . body . userid ;
const username = req . body . username ;
const plaintextPassword = req . body . password ;
if ( userid !== 'admin' && ! config _api . getConfigItem ( 'ytdl_allow_registration' ) && ! req . isAuthenticated ( ) && ( ! req . user || ! exports . userHasPermission ( req . user . uid , 'settings' ) ) ) {
logger . error ( ` Registration failed for user ${ userid } . Registration is disabled. ` ) ;
res . sendStatus ( 409 ) ;
return ;
}
if ( plaintextPassword === "" ) {
logger . error ( ` Registration failed for user ${ userid } . A password must be provided. ` ) ;
res . sendStatus ( 409 ) ;
return ;
}
if ( ! userid || ! username ) {
logger . error ( ` Registration failed for user ${ userid } . Username or userid is invalid. ` ) ;
}
const new _user = await auth _api . registerUser ( userid , username , plaintextPassword ) ;
if ( ! new _user ) {
res . sendStatus ( 409 ) ;
return ;
}
res . send ( {
user : new _user
} ) ;
} ) ;
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 = await auth _api . adminExists ( ) ;
res . send ( { exists : exists } ) ;
} ) ;
// user management
app . post ( '/api/getUsers' , optionalJwt , async ( req , res ) => {
let users = await db _api . getRecords ( 'users' ) ;
res . send ( { users : users } ) ;
} ) ;
app . post ( '/api/getRoles' , optionalJwt , async ( req , res ) => {
let roles = await db _api . getRecords ( 'roles' ) ;
res . send ( { roles : roles } ) ;
} ) ;
app . post ( '/api/updateUser' , optionalJwt , async ( req , res ) => {
let change _obj = req . body . change _object ;
try {
if ( change _obj . name ) {
await db _api . updateRecord ( 'users' , { uid : change _obj . uid } , { name : change _obj . name } ) ;
}
if ( change _obj . role ) {
await db _api . updateRecord ( 'users' , { uid : change _obj . uid } , { role : change _obj . role } ) ;
}
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 {
const success = await auth _api . deleteUser ( uid ) ;
res . send ( { success : success } ) ;
} 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 = await 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 = await auth _api . changeRolePermissions ( role , permission , new _value ) ;
res . send ( { success : success } ) ;
} ) ;
// notifications
app . post ( '/api/getNotifications' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const notifications = await db _api . getRecords ( 'notifications' , { user _uid : uuid } ) ;
res . send ( { notifications : notifications } ) ;
} ) ;
// set notifications to read
app . post ( '/api/setNotificationsToRead' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const success = await db _api . updateRecords ( 'notifications' , { user _uid : uuid } , { read : true } ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/deleteNotification' , optionalJwt , async ( req , res ) => {
const uid = req . isAuthenticated ( ) ? req . user . uid : null ;
const success = await db _api . removeRecord ( 'notifications' , { uid : uid } ) ;
res . send ( { success : success } ) ;
} ) ;
app . post ( '/api/deleteAllNotifications' , optionalJwt , async ( req , res ) => {
const uuid = req . isAuthenticated ( ) ? req . user . uid : null ;
const success = await db _api . removeAllRecords ( 'notifications' , { user _uid : uuid } ) ;
res . send ( { success : success } ) ;
} ) ;
// rss feed
app . get ( '/api/rss' , async function ( req , res ) {
if ( ! config _api . getConfigItem ( 'ytdl_enable_rss_feed' ) ) {
logger . error ( 'RSS feed is disabled! It must be enabled in the settings before it can be generated.' ) ;
res . sendStatus ( 403 ) ;
return ;
}
// these are returned
const sort = req . query . sort ? JSON . parse ( decodeURIComponent ( req . query . sort ) ) : { by : 'registered' , order : - 1 } ;
const range = req . query . range ? req . query . range . map ( range _num => parseInt ( range _num ) ) : null ;
const text _search = req . query . text _search ? decodeURIComponent ( req . query . text _search ) : null ;
const file _type _filter = req . query . file _type _filter ;
const favorite _filter = req . query . favorite _filter === 'true' ;
const sub _id = req . query . sub _id ? decodeURIComponent ( req . query . sub _id ) : null ;
const uuid = req . query . uuid ? decodeURIComponent ( req . query . uuid ) : null ;
const { files } = await files _api . getAllFiles ( sort , range , text _search , file _type _filter , favorite _filter , sub _id , uuid ) ;
const feed = new Feed ( {
title : 'Downloads' ,
description : 'YoutubeDL-Material downloads' ,
id : utils . getBaseURL ( ) ,
link : utils . getBaseURL ( ) ,
image : 'https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/src/assets/images/logo_128px.png' ,
favicon : 'https://raw.githubusercontent.com/Tzahi12345/YoutubeDL-Material/master/src/favicon.ico' ,
generator : 'YoutubeDL-Material'
} ) ;
files . forEach ( file => {
feed . addItem ( {
title : file . title ,
link : ` ${ utils . getBaseURL ( ) } /#/player;uid= ${ file . uid } ` ,
description : file . description ,
author : [
{
name : file . uploader ,
link : file . url
}
] ,
contributor : [ ] ,
date : file . timestamp ,
// https://stackoverflow.com/a/45415677/8088021
image : file . thumbnailURL . replace ( '&' , '&' )
} ) ;
} ) ;
res . send ( feed . rss2 ( ) ) ;
} ) ;
// web server
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 ) ) ;