var async = require ( 'async' ) ;
const { uuid } = require ( 'uuidv4' ) ;
var fs = require ( 'fs-extra' ) ;
var winston = require ( 'winston' ) ;
var path = require ( 'path' ) ;
var youtubedl = require ( 'youtube-dl' ) ;
var compression = require ( 'compression' ) ;
var https = require ( 'https' ) ;
var express = require ( "express" ) ;
var bodyParser = require ( "body-parser" ) ;
var archiver = require ( 'archiver' ) ;
var unzipper = require ( 'unzipper' ) ;
var mergeFiles = require ( 'merge-files' ) ;
const low = require ( 'lowdb' )
var ProgressBar = require ( 'progress' ) ;
var md5 = require ( 'md5' ) ;
const NodeID3 = require ( 'node-id3' )
const downloader = require ( 'youtube-dl/lib/downloader' )
const fetch = require ( 'node-fetch' ) ;
var URL = require ( 'url' ) . URL ;
const shortid = require ( 'shortid' )
const url _api = require ( 'url' ) ;
var config _api = require ( './config.js' ) ;
var subscriptions _api = require ( './subscriptions' )
const CONSTS = require ( './consts' )
const { spawn } = require ( 'child_process' )
const is _windows = process . platform === 'win32' ;
var app = express ( ) ;
// database setup
const FileSync = require ( 'lowdb/adapters/FileSync' )
const adapter = new FileSync ( './appdata/db.json' ) ;
const db = low ( adapter )
// 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' } )
]
} ) ;
config _api . setLogger ( logger ) ;
subscriptions _api . setLogger ( logger ) ;
// var GithubContent = require('github-content');
// Set some defaults
db . defaults (
{
playlists : {
audio : [ ] ,
video : [ ]
} ,
files : {
audio : [ ] ,
video : [ ]
} ,
configWriteFlag : false ,
downloads : { } ,
subscriptions : [ ] ,
pin _md5 : '' ,
files _to _db _migration _complete : false
} ) . write ( ) ;
// config values
var frontendUrl = null ;
var backendUrl = null ;
var backendPort = null ;
var usingEncryption = null ;
var basePath = null ;
var audioFolderPath = null ;
var videoFolderPath = null ;
var downloadOnlyMode = null ;
var useDefaultDownloadingAgent = null ;
var customDownloadingAgent = null ;
var allowSubscriptions = null ;
var subscriptionsCheckInterval = null ;
var archivePath = path . join ( _ _dirname , 'appdata' , 'archives' ) ;
// other needed values
var options = null ; // encryption options
var url _domain = null ;
var updaterStatus = null ;
var last _downloads _check = null ;
var downloads _check _interval = 1000 ;
var timestamp _server _start = Date . now ( ) ;
if ( debugMode ) logger . info ( 'YTDL-Material in debug mode!' ) ;
// check if just updated
const just _restarted = fs . existsSync ( 'restart.json' ) ;
if ( just _restarted ) {
updaterStatus = {
updating : false ,
details : 'Update complete! You are now on ' + CONSTS [ 'CURRENT_VERSION' ]
}
fs . unlinkSync ( 'restart.json' ) ;
}
// updates & starts youtubedl
startYoutubeDL ( ) ;
var validDownloadingAgents = [
'aria2c' ,
'avconv' ,
'axel' ,
'curl' ,
'ffmpeg' ,
'httpie' ,
'wget'
]
// don't overwrite config if it already happened.. NOT
// let alreadyWritten = db.get('configWriteFlag').value();
let writeConfigMode = process . env . write _ytdl _config ;
var config = null ;
// checks if config exists, if not, a config is auto generated
config _api . configExistsCheck ( ) ;
if ( writeConfigMode ) {
setAndLoadConfig ( ) ;
} else {
loadConfig ( ) ;
}
var downloads = { } ;
var descriptors = { } ;
app . use ( bodyParser . urlencoded ( { extended : false } ) ) ;
app . use ( bodyParser . json ( ) ) ;
// objects
function File ( id , title , thumbnailURL , isAudio , duration , url , uploader , size , path , upload _date ) {
this . id = id ;
this . title = title ;
this . thumbnailURL = thumbnailURL ;
this . isAudio = isAudio ;
this . duration = duration ;
this . url = url ;
this . uploader = uploader ;
this . size = size ;
this . path = path ;
this . upload _date = upload _date ;
}
// actual functions
async function checkMigrations ( ) {
return new Promise ( async resolve => {
// 3.5->3.6 migration
const files _to _db _migration _complete = db . get ( 'files_to_db_migration_complete' ) . value ( ) ;
if ( ! files _to _db _migration _complete ) {
logger . info ( 'Beginning migration: 3.5->3.6+' )
runFilesToDBMigration ( ) . then ( success => {
if ( success ) { logger . info ( '3.5->3.6+ migration complete!' ) ; }
else { logger . error ( 'Migration failed: 3.5->3.6+' ) ; }
} ) ;
}
resolve ( true ) ;
} ) ;
}
async function runFilesToDBMigration ( ) {
return new Promise ( async resolve => {
try {
let mp3s = getMp3s ( ) ;
let mp4s = getMp4s ( ) ;
for ( let i = 0 ; i < mp3s . length ; i ++ ) {
let file _obj = mp3s [ i ] ;
const file _already _in _db = db . get ( 'files.audio' ) . find ( { id : file _obj . id } ) . value ( ) ;
if ( ! file _already _in _db ) {
logger . verbose ( ` Migrating file ${ file _obj . id } ` ) ;
registerFileDB ( file _obj . id + '.mp3' , 'audio' ) ;
}
}
for ( let i = 0 ; i < mp4s . length ; i ++ ) {
let file _obj = mp4s [ i ] ;
const file _already _in _db = db . get ( 'files.video' ) . find ( { id : file _obj . id } ) . value ( ) ;
if ( ! file _already _in _db ) {
logger . verbose ( ` Migrating file ${ file _obj . id } ` ) ;
registerFileDB ( file _obj . id + '.mp4' , 'video' ) ;
}
}
// sets migration to complete
db . set ( 'files_to_db_migration_complete' , true ) . write ( ) ;
resolve ( true ) ;
} catch ( err ) {
resolve ( false ) ;
}
} ) ;
}
async function startServer ( ) {
if ( process . env . USING _HEROKU && process . env . PORT ) {
// default to heroku port if using heroku
backendPort = process . env . PORT || backendPort ;
// set config to port
await setPortItemFromENV ( ) ;
}
if ( usingEncryption )
{
https . createServer ( options , app ) . listen ( backendPort , function ( ) {
logger . info ( ` YoutubeDL-Material ${ CONSTS [ 'CURRENT_VERSION' ] } started on port ${ backendPort } - using SSL ` ) ;
} ) ;
}
else
{
app . listen ( backendPort , function ( ) {
logger . info ( ` YoutubeDL-Material ${ CONSTS [ 'CURRENT_VERSION' ] } started on PORT ${ backendPort } ` ) ;
} ) ;
}
}
async function restartServer ( ) {
const restartProcess = ( ) => {
spawn ( 'node' , [ 'app.js' ] , {
detached : true ,
stdio : 'inherit'
} ) . unref ( )
process . exit ( )
}
logger . info ( 'Update complete! Restarting server...' ) ;
// the following line restarts the server through nodemon
fs . writeFileSync ( 'restart.json' , 'internal use only' ) ;
}
async function updateServer ( tag ) {
// no tag provided means update to the latest version
if ( ! tag ) {
const new _version _available = await isNewVersionAvailable ( ) ;
if ( ! new _version _available ) {
logger . error ( 'ERROR: Failed to update - no update is available.' ) ;
return false ;
}
}
return new Promise ( async resolve => {
// backup current dir
updaterStatus = {
updating : true ,
'details' : 'Backing up key server files...'
}
let backup _succeeded = await backupServerLite ( ) ;
if ( ! backup _succeeded ) {
resolve ( false ) ;
return false ;
}
updaterStatus = {
updating : true ,
'details' : 'Downloading requested release...'
}
// grab new package.json and public folder
await downloadReleaseFiles ( tag ) ;
updaterStatus = {
updating : true ,
'details' : 'Installing new dependencies...'
}
// run npm install
await installDependencies ( ) ;
updaterStatus = {
updating : true ,
'details' : 'Update complete! Restarting server...'
}
restartServer ( ) ;
} , err => {
updaterStatus = {
updating : false ,
error : true ,
'details' : 'Update failed. Check error logs for more info.'
}
} ) ;
}
async function downloadReleaseFiles ( tag ) {
tag = tag ? tag : await getLatestVersion ( ) ;
return new Promise ( async resolve => {
logger . info ( 'Downloading new files...' )
// downloads the latest release zip file
await downloadReleaseZip ( tag ) ;
// deletes contents of public dir
fs . removeSync ( path . join ( _ _dirname , 'public' ) ) ;
fs . mkdirSync ( path . join ( _ _dirname , 'public' ) ) ;
let replace _ignore _list = [ 'youtubedl-material/appdata/default.json' ,
'youtubedl-material/appdata/db.json' ]
logger . info ( ` Installing update ${ tag } ... ` )
// downloads new package.json and adds new public dir files from the downloaded zip
fs . createReadStream ( path . join ( _ _dirname , ` youtubedl-material-release- ${ tag } .zip ` ) ) . pipe ( unzipper . Parse ( ) )
. on ( 'entry' , function ( entry ) {
var fileName = entry . path ;
var type = entry . type ; // 'Directory' or 'File'
var size = entry . size ;
var is _dir = fileName . substring ( fileName . length - 1 , fileName . length ) === '/'
if ( ! is _dir && fileName . includes ( 'youtubedl-material/public/' ) ) {
// get public folder files
var actualFileName = fileName . replace ( 'youtubedl-material/public/' , '' ) ;
if ( actualFileName . length !== 0 && actualFileName . substring ( actualFileName . length - 1 , actualFileName . length ) !== '/' ) {
fs . ensureDirSync ( path . join ( _ _dirname , 'public' , path . dirname ( actualFileName ) ) ) ;
entry . pipe ( fs . createWriteStream ( path . join ( _ _dirname , 'public' , actualFileName ) ) ) ;
} else {
entry . autodrain ( ) ;
}
} else if ( ! is _dir && ! replace _ignore _list . includes ( fileName ) ) {
// get package.json
var actualFileName = fileName . replace ( 'youtubedl-material/' , '' ) ;
logger . verbose ( 'Downloading file ' + actualFileName ) ;
entry . pipe ( fs . createWriteStream ( path . join ( _ _dirname , actualFileName ) ) ) ;
} else {
entry . autodrain ( ) ;
}
} )
. on ( 'close' , function ( ) {
resolve ( true ) ;
} ) ;
} ) ;
}
// helper function to download file using fetch
async function fetchFile ( url , path , file _label ) {
var len = null ;
const res = await fetch ( url ) ;
len = parseInt ( res . headers . get ( "Content-Length" ) , 10 ) ;
var bar = new ProgressBar ( ` Downloading ${ file _label } [:bar] :percent :etas ` , {
complete : '=' ,
incomplete : ' ' ,
width : 20 ,
total : len
} ) ;
const fileStream = fs . createWriteStream ( path ) ;
await new Promise ( ( resolve , reject ) => {
res . body . pipe ( fileStream ) ;
res . body . on ( "error" , ( err ) => {
reject ( err ) ;
} ) ;
res . body . on ( 'data' , function ( chunk ) {
bar . tick ( chunk . length ) ;
} ) ;
fileStream . on ( "finish" , function ( ) {
resolve ( ) ;
} ) ;
} ) ;
}
async function downloadReleaseZip ( tag ) {
return new Promise ( async resolve => {
// get name of zip file, which depends on the version
const latest _release _link = ` https://github.com/Tzahi12345/YoutubeDL-Material/releases/download/ ${ tag } / ` ;
const tag _without _v = tag . substring ( 1 , tag . length ) ;
const zip _file _name = ` youtubedl-material- ${ tag _without _v } .zip `
const latest _zip _link = latest _release _link + zip _file _name ;
let output _path = path . join ( _ _dirname , ` youtubedl-material-release- ${ tag } .zip ` ) ;
// download zip from release
await fetchFile ( latest _zip _link , output _path , 'update ' + tag ) ;
resolve ( true ) ;
} ) ;
}
async function installDependencies ( ) {
return new Promise ( resolve => {
var child _process = require ( 'child_process' ) ;
child _process . execSync ( 'npm install' , { stdio : [ 0 , 1 , 2 ] } ) ;
resolve ( true ) ;
} ) ;
}
async function backupServerLite ( ) {
return new Promise ( async resolve => {
fs . ensureDirSync ( path . join ( _ _dirname , 'appdata' , 'backups' ) ) ;
let output _path = path . join ( 'appdata' , 'backups' , ` backup- ${ Date . now ( ) } .zip ` ) ;
logger . info ( ` Backing up your non-video/audio files to ${ output _path } . This may take up to a few seconds/minutes. ` ) ;
let output = fs . createWriteStream ( path . join ( _ _dirname , output _path ) ) ;
var archive = archiver ( 'zip' , {
gzip : true ,
zlib : { level : 9 } // Sets the compression level.
} ) ;
archive . on ( 'error' , function ( err ) {
logger . error ( err ) ;
resolve ( false ) ;
} ) ;
// pipe archive data to the output file
archive . pipe ( output ) ;
// ignore certain directories (ones with video or audio files)
const files _to _ignore = [ path . join ( config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) , '**' ) ,
path . join ( config _api . getConfigItem ( 'ytdl_audio_folder_path' ) , '**' ) ,
path . join ( config _api . getConfigItem ( 'ytdl_video_folder_path' ) , '**' ) ,
'appdata/backups/backup-*.zip' ] ;
archive . glob ( '**/*' , {
ignore : files _to _ignore
} ) ;
await archive . finalize ( ) ;
// wait a tiny bit for the zip to reload in fs
setTimeout ( function ( ) {
resolve ( true ) ;
} , 100 ) ;
} ) ;
}
async function isNewVersionAvailable ( ) {
return new Promise ( async resolve => {
// gets tag of the latest version of youtubedl-material, compare to current version
const latest _tag = await getLatestVersion ( ) ;
const current _tag = CONSTS [ 'CURRENT_VERSION' ] ;
if ( latest _tag > current _tag ) {
resolve ( true ) ;
} else {
resolve ( false ) ;
}
} ) ;
}
async function getLatestVersion ( ) {
return new Promise ( resolve => {
fetch ( 'https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest' , { method : 'Get' } )
. then ( async res => res . json ( ) )
. then ( async ( json ) => {
if ( json [ 'message' ] ) {
// means there's an error in getting latest version
logger . error ( ` ERROR: Received the following message from GitHub's API: ` ) ;
logger . error ( json [ 'message' ] ) ;
if ( json [ 'documentation_url' ] ) logger . error ( ` Associated URL: ${ json [ 'documentation_url' ] } ` )
}
resolve ( json [ 'tag_name' ] ) ;
return ;
} ) ;
} ) ;
}
async function setPortItemFromENV ( ) {
return new Promise ( resolve => {
config _api . setConfigItem ( 'ytdl_port' , backendPort . toString ( ) ) ;
setTimeout ( ( ) => resolve ( true ) , 100 ) ;
} ) ;
}
async function setAndLoadConfig ( ) {
await setConfigFromEnv ( ) ;
await loadConfig ( ) ;
}
async function setConfigFromEnv ( ) {
return new Promise ( resolve => {
let config _items = getEnvConfigItems ( ) ;
let success = config _api . setConfigItems ( config _items ) ;
if ( success ) {
logger . info ( 'Config items set using ENV variables.' ) ;
setTimeout ( ( ) => resolve ( true ) , 100 ) ;
} else {
logger . error ( 'ERROR: Failed to set config items using ENV variables.' ) ;
resolve ( false ) ;
}
} ) ;
}
async function loadConfig ( ) {
return new Promise ( async resolve => {
url = ! debugMode ? config _api . getConfigItem ( 'ytdl_url' ) : 'http://localhost:4200' ;
backendPort = config _api . getConfigItem ( 'ytdl_port' ) ;
usingEncryption = config _api . getConfigItem ( 'ytdl_use_encryption' ) ;
audioFolderPath = config _api . getConfigItem ( 'ytdl_audio_folder_path' ) ;
videoFolderPath = config _api . getConfigItem ( 'ytdl_video_folder_path' ) ;
downloadOnlyMode = config _api . getConfigItem ( 'ytdl_download_only_mode' ) ;
useDefaultDownloadingAgent = config _api . getConfigItem ( 'ytdl_use_default_downloading_agent' ) ;
customDownloadingAgent = config _api . getConfigItem ( 'ytdl_custom_downloading_agent' ) ;
allowSubscriptions = config _api . getConfigItem ( 'ytdl_allow_subscriptions' ) ;
subscriptionsCheckInterval = config _api . getConfigItem ( 'ytdl_subscriptions_check_interval' ) ;
if ( ! useDefaultDownloadingAgent && validDownloadingAgents . indexOf ( customDownloadingAgent ) !== - 1 ) {
logger . info ( ` Using non-default downloading agent \' ${ customDownloadingAgent } \' ` )
} else {
customDownloadingAgent = null ;
}
if ( usingEncryption )
{
var certFilePath = path . resolve ( config _api . getConfigItem ( 'ytdl_cert_file_path' ) ) ;
var keyFilePath = path . resolve ( config _api . getConfigItem ( 'ytdl_key_file_path' ) ) ;
var certKeyFile = fs . readFileSync ( keyFilePath ) ;
var certFile = fs . readFileSync ( certFilePath ) ;
options = {
key : certKeyFile ,
cert : certFile
} ;
}
url _domain = new URL ( url ) ;
// creates archive path if missing
if ( ! fs . existsSync ( archivePath ) ) {
fs . mkdirSync ( archivePath ) ;
}
// get subscriptions
if ( allowSubscriptions ) {
// runs initially, then runs every ${subscriptionCheckInterval} seconds
watchSubscriptions ( ) ;
setInterval ( ( ) => {
watchSubscriptions ( ) ;
} , subscriptionsCheckInterval * 1000 ) ;
}
// check migrations
await checkMigrations ( ) ;
// load in previous downloads
downloads = db . get ( 'downloads' ) . value ( ) ;
// start the server here
startServer ( ) ;
resolve ( true ) ;
} ) ;
}
function calculateSubcriptionRetrievalDelay ( amount ) {
// frequency is 5 mins
let frequency _in _ms = subscriptionsCheckInterval * 1000 ;
let minimum _frequency = 60 * 1000 ;
const first _frequency = frequency _in _ms / amount ;
return ( first _frequency < minimum _frequency ) ? minimum _frequency : first _frequency ;
}
function watchSubscriptions ( ) {
let subscriptions = subscriptions _api . getAllSubscriptions ( ) ;
if ( ! subscriptions ) return ;
let subscriptions _amount = subscriptions . length ;
let delay _interval = calculateSubcriptionRetrievalDelay ( subscriptions _amount ) ;
let current _delay = 0 ;
for ( let i = 0 ; i < subscriptions . length ; i ++ ) {
let sub = subscriptions [ i ] ;
logger . verbose ( 'Watching ' + sub . name + ' with delay interval of ' + delay _interval ) ;
setTimeout ( ( ) => {
subscriptions _api . getVideosForSub ( sub ) ;
} , current _delay ) ;
current _delay += delay _interval ;
if ( current _delay >= subscriptionsCheckInterval * 1000 ) current _delay = 0 ;
}
}
function getOrigin ( ) {
return url _domain . origin ;
}
// gets a list of config items that are stored as an environment variable
function getEnvConfigItems ( ) {
let config _items = [ ] ;
let config _item _keys = Object . keys ( config _api . CONFIG _ITEMS ) ;
for ( let i = 0 ; i < config _item _keys . length ; i ++ ) {
let key = config _item _keys [ i ] ;
if ( process [ 'env' ] [ key ] ) {
const config _item = generateEnvVarConfigItem ( key ) ;
config _items . push ( config _item ) ;
}
}
return config _items ;
}
// gets value of a config item and stores it in an object
function generateEnvVarConfigItem ( key ) {
return { key : key , value : process [ 'env' ] [ key ] } ;
}
function getMp3s ( ) {
let mp3s = [ ] ;
var files = recFindByExt ( audioFolderPath , 'mp3' ) ; // fs.readdirSync(audioFolderPath);
for ( let i = 0 ; i < files . length ; i ++ ) {
let file = files [ i ] ;
var file _path = file . substring ( audioFolderPath . length , file . length ) ;
var stats = fs . statSync ( file ) ;
var id = file _path . substring ( 0 , file _path . length - 4 ) ;
var jsonobj = getJSONMp3 ( id ) ;
if ( ! jsonobj ) continue ;
var title = jsonobj . title ;
var url = jsonobj . webpage _url ;
var uploader = jsonobj . uploader ;
var upload _date = jsonobj . upload _date ;
upload _date = ` ${ upload _date . substring ( 0 , 4 ) } - ${ upload _date . substring ( 4 , 6 ) } - ${ upload _date . substring ( 6 , 8 ) } ` ;
var size = stats . size ;
var thumbnail = jsonobj . thumbnail ;
var duration = jsonobj . duration ;
var isaudio = true ;
var file _obj = new File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file , upload _date ) ;
mp3s . push ( file _obj ) ;
}
return mp3s ;
}
function getMp4s ( relative _path = true ) {
let mp4s = [ ] ;
var files = recFindByExt ( videoFolderPath , 'mp4' ) ;
for ( let i = 0 ; i < files . length ; i ++ ) {
let file = files [ i ] ;
var file _path = file . substring ( videoFolderPath . length , file . length ) ;
var stats = fs . statSync ( file ) ;
var id = file _path . substring ( 0 , file _path . length - 4 ) ;
var jsonobj = getJSONMp4 ( id ) ;
if ( ! jsonobj ) continue ;
var title = jsonobj . title ;
var url = jsonobj . webpage _url ;
var uploader = jsonobj . uploader ;
var upload _date = jsonobj . upload _date ;
upload _date = ` ${ upload _date . substring ( 0 , 4 ) } - ${ upload _date . substring ( 4 , 6 ) } - ${ upload _date . substring ( 6 , 8 ) } ` ;
var thumbnail = jsonobj . thumbnail ;
var duration = jsonobj . duration ;
var size = stats . size ;
var isaudio = false ;
var file _obj = new File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file , upload _date ) ;
mp4s . push ( file _obj ) ;
}
return mp4s ;
}
function getThumbnailMp3 ( name )
{
var obj = getJSONMp3 ( name ) ;
var thumbnailLink = obj . thumbnail ;
return thumbnailLink ;
}
function getThumbnailMp4 ( name )
{
var obj = getJSONMp4 ( name ) ;
var thumbnailLink = obj . thumbnail ;
return thumbnailLink ;
}
function getFileSizeMp3 ( name )
{
var jsonPath = audioFolderPath + name + ".mp3.info.json" ;
if ( fs . existsSync ( jsonPath ) )
var obj = JSON . parse ( fs . readFileSync ( jsonPath , 'utf8' ) ) ;
else
var obj = 0 ;
return obj . filesize ;
}
function getFileSizeMp4 ( name )
{
var jsonPath = videoFolderPath + name + ".info.json" ;
var filesize = 0 ;
if ( fs . existsSync ( jsonPath ) )
{
var obj = JSON . parse ( fs . readFileSync ( jsonPath , 'utf8' ) ) ;
var format = obj . format . substring ( 0 , 3 ) ;
for ( i = 0 ; i < obj . formats . length ; i ++ )
{
if ( obj . formats [ i ] . format _id == format )
{
filesize = obj . formats [ i ] . filesize ;
}
}
}
return filesize ;
}
function getJSONMp3 ( name , openReadPerms = false )
{
var jsonPath = audioFolderPath + name + ".info.json" ;
var alternateJsonPath = audioFolderPath + name + ".mp3.info.json" ;
var obj = null ;
if ( fs . existsSync ( jsonPath ) ) {
obj = JSON . parse ( fs . readFileSync ( jsonPath , 'utf8' ) ) ;
if ( ! is _windows && openReadPerms ) fs . chmodSync ( jsonPath , 0o755 ) ;
}
else if ( fs . existsSync ( alternateJsonPath ) ) {
obj = JSON . parse ( fs . readFileSync ( alternateJsonPath , 'utf8' ) ) ;
if ( ! is _windows && openReadPerms ) fs . chmodSync ( alternateJsonPath , 0o755 ) ;
}
else
obj = 0 ;
return obj ;
}
function getJSONMp4 ( name , customPath = null , openReadPerms = false )
{
var obj = null ; // output
let jsonPath = null ;
var alternateJsonPath = videoFolderPath + name + ".mp4.info.json" ;
if ( ! customPath ) {
jsonPath = videoFolderPath + name + ".info.json" ;
} else {
jsonPath = customPath + name + ".info.json" ;
alternateJsonPath = customPath + name + ".mp4.info.json" ;
}
if ( fs . existsSync ( jsonPath ) )
{
obj = JSON . parse ( fs . readFileSync ( jsonPath , 'utf8' ) ) ;
if ( openReadPerms ) fs . chmodSync ( jsonPath , 0o644 ) ;
} else if ( fs . existsSync ( alternateJsonPath ) ) {
obj = JSON . parse ( fs . readFileSync ( alternateJsonPath , 'utf8' ) ) ;
if ( openReadPerms ) fs . chmodSync ( alternateJsonPath , 0o644 ) ;
}
else obj = 0 ;
return obj ;
}
function getAmountDownloadedMp3 ( name )
{
var partPath = audioFolderPath + name + ".mp3.part" ;
if ( fs . existsSync ( partPath ) )
{
const stats = fs . statSync ( partPath ) ;
const fileSizeInBytes = stats . size ;
return fileSizeInBytes ;
}
else
return 0 ;
}
function getAmountDownloadedMp4 ( name )
{
var format = getVideoFormatID ( name ) ;
var partPath = videoFolderPath + name + ".f" + format + ".mp4.part" ;
if ( fs . existsSync ( partPath ) )
{
const stats = fs . statSync ( partPath ) ;
const fileSizeInBytes = stats . size ;
return fileSizeInBytes ;
}
else
return 0 ;
}
function getVideoFormatID ( name )
{
var jsonPath = videoFolderPath + name + ".info.json" ;
if ( fs . existsSync ( jsonPath ) )
{
var obj = JSON . parse ( fs . readFileSync ( jsonPath , 'utf8' ) ) ;
var format = obj . format . substring ( 0 , 3 ) ;
return format ;
}
}
async function createPlaylistZipFile ( fileNames , type , outputName , fullPathProvided = null ) {
return new Promise ( async resolve => {
let zipFolderPath = null ;
if ( ! fullPathProvided ) {
zipFolderPath = path . join ( _ _dirname , ( type === 'audio' ) ? audioFolderPath : videoFolderPath ) ;
} else {
zipFolderPath = path . join ( _ _dirname , config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ) ;
}
let ext = ( type === 'audio' ) ? '.mp3' : '.mp4' ;
let output = fs . createWriteStream ( path . join ( zipFolderPath , outputName + '.zip' ) ) ;
var archive = archiver ( 'zip' , {
gzip : true ,
zlib : { level : 9 } // Sets the compression level.
} ) ;
archive . on ( 'error' , function ( err ) {
logger . error ( err ) ;
throw err ;
} ) ;
// pipe archive data to the output file
archive . pipe ( output ) ;
for ( let i = 0 ; i < fileNames . length ; i ++ ) {
let fileName = fileNames [ i ] ;
let fileNamePathRemoved = path . parse ( fileName ) . base ;
let file _path = ! fullPathProvided ? zipFolderPath + fileName + ext : fileName ;
archive . file ( file _path , { name : fileNamePathRemoved + ext } )
}
await archive . finalize ( ) ;
// wait a tiny bit for the zip to reload in fs
setTimeout ( function ( ) {
resolve ( path . join ( zipFolderPath , outputName + '.zip' ) ) ;
} , 100 ) ;
} ) ;
}
async function deleteAudioFile ( name , blacklistMode = false ) {
return new Promise ( resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
var jsonPath = path . join ( audioFolderPath , name + '.mp3.info.json' ) ;
var altJSONPath = path . join ( audioFolderPath , name + '.info.json' ) ;
var audioFilePath = path . join ( audioFolderPath , name + '.mp3' ) ;
jsonPath = path . join ( _ _dirname , jsonPath ) ;
altJSONPath = path . join ( _ _dirname , altJSONPath ) ;
audioFilePath = path . join ( _ _dirname , audioFilePath ) ;
let jsonExists = fs . existsSync ( jsonPath ) ;
if ( ! jsonExists ) {
if ( fs . existsSync ( altJSONPath ) ) {
jsonExists = true ;
jsonPath = altJSONPath ;
}
}
let audioFileExists = fs . existsSync ( audioFilePath ) ;
if ( descriptors [ name ] ) {
try {
for ( let i = 0 ; i < descriptors [ name ] . length ; i ++ ) {
descriptors [ name ] [ i ] . destroy ( ) ;
}
} catch ( e ) {
}
}
let useYoutubeDLArchive = config _api . getConfigItem ( 'ytdl_use_youtubedl_archive' ) ;
if ( useYoutubeDLArchive ) {
const archive _path = path . join ( archivePath , 'archive_audio.txt' ) ;
// get ID from JSON
var jsonobj = getJSONMp3 ( name ) ;
let id = null ;
if ( jsonobj ) id = jsonobj . id ;
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if ( fs . existsSync ( archive _path ) ) {
const line = id ? subscriptions _api . removeIDFromArchive ( archive _path , id ) : null ;
if ( blacklistMode && line ) writeToBlacklist ( 'audio' , line ) ;
} else {
logger . info ( 'Could not find archive file for audio files. Creating...' ) ;
fs . closeSync ( fs . openSync ( archive _path , 'w' ) ) ;
}
}
if ( jsonExists ) fs . unlinkSync ( jsonPath ) ;
if ( audioFileExists ) {
fs . unlink ( audioFilePath , function ( err ) {
if ( fs . existsSync ( jsonPath ) || fs . existsSync ( audioFilePath ) ) {
resolve ( false ) ;
} else {
resolve ( true ) ;
}
} ) ;
} else {
// TODO: tell user that the file didn't exist
resolve ( true ) ;
}
} ) ;
}
async function deleteVideoFile ( name , customPath = null , blacklistMode = false ) {
return new Promise ( resolve => {
let filePath = customPath ? customPath : videoFolderPath ;
var jsonPath = path . join ( filePath , name + '.info.json' ) ;
var videoFilePath = path . join ( filePath , name + '.mp4' ) ;
jsonPath = path . join ( _ _dirname , jsonPath ) ;
videoFilePath = path . join ( _ _dirname , videoFilePath ) ;
jsonExists = fs . existsSync ( jsonPath ) ;
videoFileExists = fs . existsSync ( videoFilePath ) ;
if ( descriptors [ name ] ) {
try {
for ( let i = 0 ; i < descriptors [ name ] . length ; i ++ ) {
descriptors [ name ] [ i ] . destroy ( ) ;
}
} catch ( e ) {
}
}
let useYoutubeDLArchive = config _api . getConfigItem ( 'ytdl_use_youtubedl_archive' ) ;
if ( useYoutubeDLArchive ) {
const archive _path = path . join ( archivePath , 'archive_video.txt' ) ;
// get ID from JSON
var jsonobj = getJSONMp4 ( name ) ;
let id = null ;
if ( jsonobj ) id = jsonobj . id ;
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if ( fs . existsSync ( archive _path ) ) {
const line = id ? subscriptions _api . removeIDFromArchive ( archive _path , id ) : null ;
if ( blacklistMode && line ) writeToBlacklist ( 'video' , line ) ;
} else {
logger . info ( 'Could not find archive file for videos. Creating...' ) ;
fs . closeSync ( fs . openSync ( archive _path , 'w' ) ) ;
}
}
if ( jsonExists ) fs . unlinkSync ( jsonPath ) ;
if ( videoFileExists ) {
fs . unlink ( videoFilePath , function ( err ) {
if ( fs . existsSync ( jsonPath ) || fs . existsSync ( videoFilePath ) ) {
resolve ( false ) ;
} else {
resolve ( true ) ;
}
} ) ;
} else {
// TODO: tell user that the file didn't exist
resolve ( true ) ;
}
} ) ;
}
function recFindByExt ( base , ext , files , result )
{
files = files || fs . readdirSync ( base )
result = result || [ ]
files . forEach (
function ( file ) {
var newbase = path . join ( base , file )
if ( fs . statSync ( newbase ) . isDirectory ( ) )
{
result = recFindByExt ( newbase , ext , fs . readdirSync ( newbase ) , result )
}
else
{
if ( file . substr ( - 1 * ( ext . length + 1 ) ) == '.' + ext )
{
result . push ( newbase )
}
}
}
)
return result
}
function registerFileDB ( full _file _path , type ) {
const file _id = full _file _path . substring ( 0 , full _file _path . length - 4 ) ;
const file _object = generateFileObject ( file _id , type ) ;
if ( ! file _object ) {
logger . error ( ` Could not find associated JSON file for ${ type } file ${ file _id } ` ) ;
return false ;
}
file _object [ 'uid' ] = uuid ( ) ;
file _object [ 'registered' ] = Date . now ( ) ;
path _object = path . parse ( file _object [ 'path' ] ) ;
file _object [ 'path' ] = path . format ( path _object ) ;
db . get ( ` files. ${ type } ` )
. push ( file _object )
. write ( ) ;
return file _object [ 'uid' ] ;
}
function generateFileObject ( id , type ) {
var jsonobj = ( type === 'audio' ) ? getJSONMp3 ( id , true ) : getJSONMp4 ( id , null , true ) ;
if ( ! jsonobj ) {
return null ;
}
const ext = ( type === 'audio' ) ? '.mp3' : '.mp4'
const file _path = getTrueFileName ( jsonobj [ '_filename' ] , type ) ; // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
var stats = fs . statSync ( path . join ( _ _dirname , file _path ) ) ;
var title = jsonobj . title ;
var url = jsonobj . webpage _url ;
var uploader = jsonobj . uploader ;
var upload _date = jsonobj . upload _date ;
upload _date = upload _date ? ` ${ upload _date . substring ( 0 , 4 ) } - ${ upload _date . substring ( 4 , 6 ) } - ${ upload _date . substring ( 6 , 8 ) } ` : 'N/A' ;
var size = stats . size ;
var thumbnail = jsonobj . thumbnail ;
var duration = jsonobj . duration ;
var isaudio = type === 'audio' ;
var file _obj = new File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file _path , upload _date ) ;
return file _obj ;
}
// replaces .webm with appropriate extension
function getTrueFileName ( unfixed _path , type ) {
let fixed _path = unfixed _path ;
const new _ext = ( type === 'audio' ? 'mp3' : 'mp4' ) ;
let unfixed _parts = unfixed _path . split ( '.' ) ;
const old _ext = unfixed _parts [ unfixed _parts . length - 1 ] ;
if ( old _ext !== new _ext ) {
unfixed _parts [ unfixed _parts . length - 1 ] = new _ext ;
fixed _path = unfixed _parts . join ( '.' ) ;
}
return fixed _path ;
}
function getAudioInfos ( fileNames ) {
let result = [ ] ;
for ( let i = 0 ; i < fileNames . length ; i ++ ) {
let fileName = fileNames [ i ] ;
let fileLocation = audioFolderPath + fileName + '.mp3.info.json' ;
if ( fs . existsSync ( fileLocation ) ) {
let data = fs . readFileSync ( fileLocation ) ;
try {
result . push ( JSON . parse ( data ) ) ;
} catch ( e ) {
logger . error ( ` Could not find info for file ${ fileName } .mp3 ` ) ;
}
}
}
return result ;
}
function getVideoInfos ( fileNames ) {
let result = [ ] ;
for ( let i = 0 ; i < fileNames . length ; i ++ ) {
let fileName = fileNames [ i ] ;
let fileLocation = videoFolderPath + fileName + '.info.json' ;
if ( fs . existsSync ( fileLocation ) ) {
let data = fs . readFileSync ( fileLocation ) ;
try {
result . push ( JSON . parse ( data ) ) ;
} catch ( e ) {
logger . error ( ` Could not find info for file ${ fileName } .mp4 ` ) ;
}
}
}
return result ;
}
// downloads
async function downloadFileByURL _exec ( url , type , options , sessionID = null ) {
return new Promise ( async resolve => {
var date = Date . now ( ) ;
// audio / video specific vars
var is _audio = type === 'audio' ;
var ext = is _audio ? '.mp3' : '.mp4' ;
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath ;
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 ,
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 ( ) ;
await new Promise ( resolve => {
youtubedl . exec ( url , [ ... downloadConfig , '--dump-json' ] , { } , function ( err , output ) {
if ( output ) {
let json = JSON . parse ( output [ 0 ] ) ;
const output _no _ext = removeFileExtension ( json [ '_filename' ] ) ;
download [ 'expected_path' ] = output _no _ext + ext ;
download [ 'expected_json_path' ] = output _no _ext + '.info.json' ;
resolve ( true ) ;
} else if ( err ) {
logger . error ( err . stderr ) ;
} else {
logger . error ( ` Video info retrieval failed. Download progress will be unavailable for URL ${ url } ` ) ;
}
} ) ;
} ) ;
youtubedl . exec ( url , downloadConfig , { } , function ( err , output ) {
download [ 'downloading' ] = false ;
download [ 'timestamp_end' ] = Date . now ( ) ;
var file _uid = null ;
let new _date = Date . now ( ) ;
let difference = ( new _date - date ) / 1000 ;
logger . debug ( ` Video download delay: ${ difference } seconds. ` ) ;
if ( err ) {
logger . error ( err . stderr ) ;
download [ 'error' ] = err . stderr ;
updateDownloads ( ) ;
resolve ( false ) ;
throw err ;
} else if ( output ) {
if ( output . length === 0 || output [ 0 ] . length === 0 ) {
download [ 'error' ] = 'No output. Check if video already exists in your archive.' ;
updateDownloads ( ) ;
resolve ( false ) ;
return ;
}
var file _names = [ ] ;
for ( let i = 0 ; i < output . length ; i ++ ) {
let output _json = null ;
try {
output _json = JSON . parse ( output [ i ] ) ;
} catch ( e ) {
output _json = null ;
}
var modified _file _name = output _json ? output _json [ 'title' ] : null ;
if ( ! output _json ) {
continue ;
}
// get filepath with no extension
const filepath _no _extension = removeFileExtension ( output _json [ '_filename' ] ) ;
var full _file _path = filepath _no _extension + ext ;
var file _name = filepath _no _extension . substring ( fileFolderPath . length , filepath _no _extension . length ) ;
// renames file if necessary due to bug
if ( ! fs . existsSync ( output _json [ '_filename' ] && fs . existsSync ( output _json [ '_filename' ] + '.webm' ) ) ) {
try {
fs . renameSync ( output _json [ '_filename' ] + '.webm' , output _json [ '_filename' ] ) ;
logger . info ( 'Renamed ' + file _name + '.webm to ' + file _name ) ;
} catch ( e ) {
}
}
if ( type === 'audio' ) {
let tags = {
title : output _json [ 'title' ] ,
artist : output _json [ 'artist' ] ? output _json [ 'artist' ] : output _json [ 'uploader' ]
}
let success = NodeID3 . write ( tags , output _json [ '_filename' ] ) ;
if ( ! success ) logger . error ( 'Failed to apply ID3 tag to audio file ' + output _json [ '_filename' ] ) ;
}
// registers file in DB
file _uid = registerFileDB ( full _file _path . substring ( fileFolderPath . length , full _file _path . length ) , 'video' ) ;
if ( file _name ) file _names . push ( file _name ) ;
}
let is _playlist = file _names . length > 1 ;
if ( options . merged _string ) {
let current _merged _archive = fs . readFileSync ( fileFolderPath + 'merged.txt' , 'utf8' ) ;
let diff = current _merged _archive . replace ( options . merged _string , '' ) ;
const archive _path = path . join ( archivePath , ` archive_ ${ type } .txt ` ) ;
fs . appendFileSync ( archive _path , diff ) ;
}
download [ 'complete' ] = true ;
updateDownloads ( ) ;
var videopathEncoded = encodeURIComponent ( file _names [ 0 ] ) ;
resolve ( {
[ ( type === 'audio' ) ? 'audiopathEncoded' : 'videopathEncoded' ] : videopathEncoded ,
file _names : is _playlist ? file _names : null ,
uid : file _uid
} ) ;
}
} ) ;
} ) ;
}
async function downloadFileByURL _normal ( url , type , options , sessionID = null ) {
return new Promise ( async resolve => {
var date = Date . now ( ) ;
var file _uid = null ;
var fileFolderPath = type === 'audio' ? audioFolderPath : videoFolderPath ;
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 ,
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 ;
console . log ( 'Download started' )
fs . writeJSONSync ( removeFileExtension ( video _info . _filename ) + '.info.json' , video _info ) ;
video . pipe ( fs . createWriteStream ( video _info . _filename , { flags : 'w' } ) )
} ) ;
// Will be called if download was already completed and there is nothing more to download.
video . on ( 'complete' , function complete ( info ) {
'use strict'
console . log ( 'filename: ' + info . _filename + ' already downloaded.' )
} )
let download _pos = 0 ;
video . on ( 'data' , function data ( chunk ) {
download _pos += chunk . length
// `size` should not be 0 here.
if ( file _size ) {
let percent = ( download _pos / file _size * 100 ) . toFixed ( 2 )
download [ 'percent_complete' ] = percent ;
}
} ) ;
video . on ( 'end' , function ( ) {
console . log ( 'finished downloading!' )
let new _date = Date . now ( ) ;
let difference = ( new _date - date ) / 1000 ;
logger . debug ( ` Video download delay: ${ difference } seconds. ` ) ;
download [ 'complete' ] = true ;
updateDownloads ( ) ;
// Does ID3 tagging if audio
if ( type === 'audio' ) {
let tags = {
title : video _info [ 'title' ] ,
artist : video _info [ 'artist' ] ? video _info [ 'artist' ] : video _info [ 'uploader' ]
}
let success = NodeID3 . write ( tags , video _info . _filename ) ;
if ( ! success ) logger . error ( 'Failed to apply ID3 tag to audio file ' + video _info . _filename ) ;
}
// registers file in DB
const base _file _name = video _info . _filename . substring ( fileFolderPath . length , video _info . _filename . length ) ;
file _uid = registerFileDB ( base _file _name , type ) ;
if ( options . merged _string ) {
let current _merged _archive = fs . readFileSync ( fileFolderPath + 'merged.txt' , 'utf8' ) ;
let diff = current _merged _archive . replace ( options . merged _string , '' ) ;
const archive _path = path . join ( archivePath , 'archive_video.txt' ) ;
fs . appendFileSync ( archive _path , diff ) ;
}
videopathEncoded = encodeURIComponent ( removeFileExtension ( base _file _name ) ) ;
resolve ( {
[ ( type === 'audio' ) ? 'audiopathEncoded' : 'videopathEncoded' ] : videopathEncoded ,
file _names : /*is_playlist ? file_names :*/ null , // playlist support is not ready
uid : file _uid
} ) ;
} ) ;
video . on ( 'error' , function error ( err ) {
logger . error ( err ) ;
download [ error ] = err ;
updateDownloads ( ) ;
resolve ( false ) ;
} ) ;
} ) ;
}
async function generateArgs ( url , type , options ) {
return new Promise ( async resolve => {
var videopath = '%(title)s' ;
var globalArgs = config _api . getConfigItem ( 'ytdl_custom_args' ) ;
var is _audio = type === 'audio' ;
var fileFolderPath = is _audio ? audioFolderPath : videoFolderPath ;
var customArgs = options . customArgs ;
var customOutput = options . customOutput ;
var customQualityConfiguration = options . customQualityConfiguration ;
// video-specific args
var selectedHeight = options . selectedHeight ;
// audio-specific args
var maxBitrate = options . maxBitrate ;
var youtubeUsername = options . youtubeUsername ;
var youtubePassword = options . youtubePassword ;
let downloadConfig = null ;
let qualityPath = is _audio ? '-f bestaudio' : '-f best[ext=mp4]' ;
if ( ! is _audio && ( url . includes ( 'tiktok' ) || url . includes ( 'pscp.tv' ) ) ) {
// tiktok videos fail when using the default format
qualityPath = '-f best' ;
}
if ( customArgs ) {
downloadConfig = customArgs . split ( ' ' ) ;
} else {
if ( customQualityConfiguration ) {
qualityPath = customQualityConfiguration ;
} else if ( selectedHeight && selectedHeight !== '' && ! is _audio ) {
qualityPath = ` -f bestvideo[height= ${ selectedHeight } ]+bestaudio/best[height= ${ selectedHeight } ] ` ;
} else if ( maxBitrate && is _audio ) {
qualityPath = ` --audio-quality ${ maxBitrate } `
}
if ( customOutput ) {
downloadConfig = [ '-o' , fileFolderPath + customOutput + "" , qualityPath , '--write-info-json' , '--print-json' ] ;
} else {
downloadConfig = [ '-o' , fileFolderPath + videopath + ( is _audio ? '.mp3' : '.mp4' ) , qualityPath , '--write-info-json' , '--print-json' ] ;
}
if ( is _audio ) {
downloadConfig . push ( '--audio-format' , 'mp3' ) ;
}
if ( youtubeUsername && youtubePassword ) {
downloadConfig . push ( '--username' , youtubeUsername , '--password' , youtubePassword ) ;
}
if ( ! useDefaultDownloadingAgent && customDownloadingAgent ) {
downloadConfig . splice ( 0 , 0 , '--external-downloader' , customDownloadingAgent ) ;
}
let useYoutubeDLArchive = config _api . getConfigItem ( 'ytdl_use_youtubedl_archive' ) ;
if ( useYoutubeDLArchive ) {
const archive _path = path . join ( archivePath , ` archive_ ${ type } .txt ` ) ;
// create archive file if it doesn't exist
if ( ! fs . existsSync ( archive _path ) ) {
fs . closeSync ( fs . openSync ( archive _path , 'w' ) ) ;
}
let blacklist _path = path . join ( archivePath , ` blacklist_ ${ type } .txt ` ) ;
// create blacklist file if it doesn't exist
if ( ! fs . existsSync ( blacklist _path ) ) {
fs . closeSync ( fs . openSync ( blacklist _path , 'w' ) ) ;
}
let merged _path = fileFolderPath + 'merged.txt' ;
fs . ensureFileSync ( merged _path ) ;
// merges blacklist and regular archive
let inputPathList = [ archive _path , blacklist _path ] ;
let status = await mergeFiles ( inputPathList , merged _path ) ;
options . merged _string = fs . readFileSync ( merged _path , "utf8" ) ;
downloadConfig . push ( '--download-archive' , merged _path ) ;
}
if ( globalArgs && globalArgs !== '' ) {
// adds global args
downloadConfig = downloadConfig . concat ( globalArgs . split ( ' ' ) ) ;
}
}
resolve ( downloadConfig ) ;
} ) ;
}
// currently only works for single urls
async function getUrlInfos ( urls ) {
let startDate = Date . now ( ) ;
let result = [ ] ;
return new Promise ( resolve => {
youtubedl . exec ( urls . join ( ' ' ) , [ '--dump-json' ] , { } , ( err , output ) => {
if ( debugMode ) {
let new _date = Date . now ( ) ;
let difference = ( new _date - startDate ) / 1000 ;
logger . info ( ` URL info retrieval delay: ${ difference } seconds. ` ) ;
}
if ( err ) {
logger . error ( 'Error during parsing:' + err ) ;
resolve ( null ) ;
}
let try _putput = null ;
try {
try _putput = JSON . parse ( output ) ;
result = try _putput ;
} catch ( e ) {
// probably multiple urls
logger . error ( 'failed to parse for urls starting with ' + urls [ 0 ] ) ;
// logger.info(output);
}
resolve ( result ) ;
} ) ;
} ) ;
}
function writeToBlacklist ( type , line ) {
let blacklistPath = path . join ( archivePath , ( type === 'audio' ) ? 'blacklist_audio.txt' : 'blacklist_video.txt' ) ;
// adds newline to the beginning of the line
line = '\n' + line ;
fs . appendFileSync ( blacklistPath , line ) ;
}
// download management functions
function updateDownloads ( ) {
db . assign ( { downloads : downloads } ) . write ( ) ;
}
/ *
function checkDownloads ( ) {
for ( let [ session _id , session _downloads ] of Object . entries ( downloads ) ) {
for ( let [ download _uid , download _obj ] of Object . entries ( session _downloads ) ) {
if ( download _obj && ! download _obj [ 'complete' ] && ! download _obj [ 'error' ]
&& download _obj . timestamp _start > timestamp _server _start ) {
// download is still running (presumably)
download _obj . percent _complete = getDownloadPercent ( download _obj ) ;
}
}
}
}
* /
function getDownloadPercent ( download _obj ) {
if ( ! download _obj . final _size ) {
if ( fs . existsSync ( download _obj . expected _json _path ) ) {
const file _json = JSON . parse ( fs . readFileSync ( download _obj . expected _json _path , 'utf8' ) ) ;
let calculated _filesize = null ;
if ( file _json [ 'format_id' ] ) {
calculated _filesize = 0 ;
const formats _used = file _json [ 'format_id' ] . split ( '+' ) ;
for ( let i = 0 ; i < file _json [ 'formats' ] . length ; i ++ ) {
if ( formats _used . includes ( file _json [ 'formats' ] [ i ] [ 'format_id' ] ) ) {
calculated _filesize += file _json [ 'formats' ] [ i ] [ 'filesize' ] ;
}
}
}
download _obj . final _size = calculated _filesize ;
} else {
console . log ( 'could not find json file' ) ;
}
}
if ( fs . existsSync ( download _obj . expected _path ) ) {
const stats = fs . statSync ( download _obj . expected _path ) ;
const size = stats . size ;
return ( size / download _obj . final _size ) * 100 ;
} else {
console . log ( 'could not find file' ) ;
return 0 ;
}
}
// youtube-dl functions
async function startYoutubeDL ( ) {
// auto update youtube-dl
await autoUpdateYoutubeDL ( ) ;
}
// auto updates the underlying youtube-dl binary, not YoutubeDL-Material
async function autoUpdateYoutubeDL ( ) {
return new Promise ( resolve => {
// get current version
let current _app _details _path = 'node_modules/youtube-dl/bin/details' ;
let current _app _details _exists = fs . existsSync ( current _app _details _path ) ;
if ( ! current _app _details _exists ) {
logger . error ( ` Failed to get youtube-dl binary details at location ' ${ current _app _details _path } '. Cancelling update check. ` ) ;
resolve ( false ) ;
return ;
}
let current _app _details = JSON . parse ( fs . readFileSync ( current _app _details _path ) ) ;
let current _version = current _app _details [ 'version' ] ;
let stored _binary _path = current _app _details [ 'path' ] ;
if ( ! stored _binary _path || typeof stored _binary _path !== 'string' ) {
// logger.info(`INFO: Failed to get youtube-dl binary path at location: ${current_app_details_path}, attempting to guess actual path...`);
const guessed _base _path = 'node_modules/youtube-dl/bin/' ;
const guessed _file _path = guessed _base _path + 'youtube-dl' + ( is _windows ? '.exe' : '' ) ;
if ( fs . existsSync ( guessed _file _path ) ) {
stored _binary _path = guessed _file _path ;
// logger.info('INFO: Guess successful! Update process continuing...')
} else {
logger . error ( ` Guess ' ${ guessed _file _path } ' is not correct. Cancelling update check. Verify that your youtube-dl binaries exist by running npm install. ` ) ;
resolve ( false ) ;
return ;
}
}
// got version, now let's check the latest version from the youtube-dl API
let youtubedl _api _path = 'https://api.github.com/repos/ytdl-org/youtube-dl/tags' ;
fetch ( youtubedl _api _path , { method : 'Get' } )
. then ( async res => res . json ( ) )
. then ( async ( json ) => {
// check if the versions are different
if ( ! json || ! json [ 0 ] ) {
resolve ( false ) ;
return false ;
}
const latest _update _version = json [ 0 ] [ 'name' ] ;
if ( current _version !== latest _update _version ) {
let binary _path = 'node_modules/youtube-dl/bin' ;
// versions different, download new update
logger . info ( 'Found new update for youtube-dl. Updating binary...' ) ;
try {
await checkExistsWithTimeout ( stored _binary _path , 10000 ) ;
} catch ( e ) {
logger . error ( ` Failed to update youtube-dl - ${ e } ` ) ;
}
downloader ( binary _path , function error ( err , done ) {
'use strict'
if ( err ) {
resolve ( false ) ;
throw err ;
}
logger . info ( ` Binary successfully updated: ${ current _version } -> ${ latest _update _version } ` ) ;
resolve ( true ) ;
} ) ;
}
} ) ;
} ) ;
}
async function checkExistsWithTimeout ( filePath , timeout ) {
return new Promise ( function ( resolve , reject ) {
var timer = setTimeout ( function ( ) {
if ( watcher ) watcher . close ( ) ;
reject ( new Error ( 'File did not exists and was not created during the timeout.' ) ) ;
} , timeout ) ;
fs . access ( filePath , fs . constants . R _OK , function ( err ) {
if ( ! err ) {
clearTimeout ( timer ) ;
watcher . close ( ) ;
resolve ( ) ;
}
} ) ;
var dir = path . dirname ( filePath ) ;
var basename = path . basename ( filePath ) ;
var watcher = fs . watch ( dir , function ( eventType , filename ) {
if ( eventType === 'rename' && filename === basename ) {
clearTimeout ( timer ) ;
watcher . close ( ) ;
resolve ( ) ;
}
} ) ;
} ) ;
}
function removeFileExtension ( filename ) {
const filename _parts = filename . split ( '.' ) ;
filename _parts . splice ( filename _parts . length - 1 )
return filename _parts . join ( '.' ) ;
}
// https://stackoverflow.com/a/32197381/8088021
const deleteFolderRecursive = function ( folder _to _delete ) {
if ( fs . existsSync ( folder _to _delete ) ) {
fs . readdirSync ( folder _to _delete ) . forEach ( ( file , index ) => {
const curPath = path . join ( folder _to _delete , file ) ;
if ( fs . lstatSync ( curPath ) . isDirectory ( ) ) { // recurse
deleteFolderRecursive ( curPath ) ;
} else { // delete file
fs . unlinkSync ( curPath ) ;
}
} ) ;
fs . rmdirSync ( folder _to _delete ) ;
}
} ;
app . use ( function ( req , res , next ) {
res . header ( "Access-Control-Allow-Headers" , "Origin, X-Requested-With, Content-Type, Accept, Authorization" ) ;
res . header ( "Access-Control-Allow-Origin" , getOrigin ( ) ) ;
if ( req . method === 'OPTIONS' ) {
res . sendStatus ( 200 ) ;
} else {
next ( ) ;
}
} ) ;
app . use ( function ( req , res , next ) {
if ( ! req . path . includes ( '/api/' ) ) {
next ( ) ;
} else if ( req . query . apiKey === admin _token ) {
next ( ) ;
} else if ( req . query . apiKey && config _api . getConfigItem ( 'ytdl_use_api_key' ) && req . query . apiKey === config _api . getConfigItem ( 'ytdl_api_key' ) ) {
next ( ) ;
} else if ( req . path . includes ( '/api/video/' ) || req . path . includes ( '/api/audio/' ) ) {
next ( ) ;
} else {
req . socket . end ( ) ;
}
} ) ;
app . use ( compression ( ) ) ;
app . get ( '/api/config' , function ( req , res ) {
let config _file = config _api . getConfigFile ( ) ;
res . send ( {
config _file : config _file ,
success : ! ! config _file
} ) ;
} ) ;
app . post ( '/api/setConfig' , function ( req , res ) {
let new _config _file = req . body . new _config _file ;
if ( new _config _file && new _config _file [ 'YoutubeDLMaterial' ] ) {
let success = config _api . setConfigFile ( new _config _file ) ;
res . send ( {
success : success
} ) ;
} else {
logger . error ( 'Tried to save invalid config file!' )
res . sendStatus ( 400 ) ;
}
} ) ;
app . get ( '/api/using-encryption' , function ( req , res ) {
res . send ( usingEncryption ) ;
} ) ;
app . post ( '/api/tomp3' , 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
}
const is _playlist = url . includes ( 'playlist' ) ;
if ( is _playlist )
result _obj = await downloadFileByURL _exec ( url , 'audio' , options , req . query . sessionID ) ;
else
result _obj = await downloadFileByURL _normal ( url , 'audio' , options , req . query . sessionID ) ;
if ( result _obj ) {
res . send ( result _obj ) ;
} else {
res . sendStatus ( 500 ) ;
}
res . end ( "yes" ) ;
} ) ;
app . post ( '/api/tomp4' , async function ( req , res ) {
var url = req . body . url ;
var options = {
customArgs : req . body . customArgs ,
customOutput : req . body . customOutput ,
selectedHeight : req . body . selectedHeight ,
customQualityConfiguration : req . body . customQualityConfiguration ,
youtubeUsername : req . body . youtubeUsername ,
youtubePassword : req . body . youtubePassword
}
const is _playlist = url . includes ( 'playlist' ) ;
let result _obj = null ;
if ( is _playlist )
result _obj = await downloadFileByURL _exec ( url , 'video' , options , req . query . sessionID ) ;
else
result _obj = await downloadFileByURL _normal ( url , 'video' , options , req . query . sessionID ) ;
if ( result _obj ) {
res . send ( result _obj ) ;
} else {
res . sendStatus ( 500 ) ;
}
res . end ( "yes" ) ;
} ) ;
// gets the status of the mp3 file that's being downloaded
app . post ( '/api/fileStatusMp3' , function ( req , res ) {
var name = decodeURIComponent ( req . body . name + "" ) ;
var exists = "" ;
var fullpath = audioFolderPath + name + ".mp3" ;
if ( fs . existsSync ( fullpath ) ) {
exists = [ basePath + audioFolderPath + name , getFileSizeMp3 ( name ) ] ;
}
else
{
var percent = 0 ;
var size = getFileSizeMp3 ( name ) ;
var downloaded = getAmountDownloadedMp3 ( name ) ;
if ( size > 0 )
percent = downloaded / size ;
exists = [ "failed" , getFileSizeMp3 ( name ) , percent ] ;
}
//logger.info(exists + " " + name);
res . send ( exists ) ;
res . end ( "yes" ) ;
} ) ;
// gets the status of the mp4 file that's being downloaded
app . post ( '/api/fileStatusMp4' , function ( req , res ) {
var name = decodeURIComponent ( req . body . name ) ;
var exists = "" ;
var fullpath = videoFolderPath + name + ".mp4" ;
if ( fs . existsSync ( fullpath ) ) {
exists = [ basePath + videoFolderPath + name , getFileSizeMp4 ( name ) ] ;
} else {
var percent = 0 ;
var size = getFileSizeMp4 ( name ) ;
var downloaded = getAmountDownloadedMp4 ( name ) ;
if ( size > 0 )
percent = downloaded / size ;
exists = [ "failed" , getFileSizeMp4 ( name ) , percent ] ;
}
//logger.info(exists + " " + name);
res . send ( exists ) ;
res . end ( "yes" ) ;
} ) ;
// gets all download mp3s
app . get ( '/api/getMp3s' , function ( req , res ) {
var mp3s = db . get ( 'files.audio' ) . value ( ) ; // getMp3s();
var playlists = db . get ( 'playlists.audio' ) . value ( ) ;
res . send ( {
mp3s : mp3s ,
playlists : playlists
} ) ;
res . end ( "yes" ) ;
} ) ;
// gets all download mp4s
app . get ( '/api/getMp4s' , function ( req , res ) {
var mp4s = db . get ( 'files.video' ) . value ( ) ; // getMp4s();
var playlists = db . get ( 'playlists.video' ) . value ( ) ;
res . send ( {
mp4s : mp4s ,
playlists : playlists
} ) ;
res . end ( "yes" ) ;
} ) ;
app . post ( '/api/getFile' , function ( req , res ) {
var uid = req . body . uid ;
var type = req . body . type ;
var file = null ;
if ( ! type ) {
file = db . get ( 'files.audio' ) . find ( { uid : uid } ) . value ( ) ;
if ( ! file ) {
file = db . get ( 'files.video' ) . find ( { uid : uid } ) . value ( ) ;
if ( file ) type = 'video' ;
} else {
type = 'audio' ;
}
}
if ( ! file && type ) db . get ( ` files. ${ type } ` ) . find ( { uid : uid } ) . value ( ) ;
if ( file ) {
res . send ( {
success : true ,
file : file
} ) ;
} else {
res . send ( {
success : false
} ) ;
}
} ) ;
// video sharing
app . post ( '/api/enableSharing' , function ( req , res ) {
var type = req . body . type ;
var uid = req . body . uid ;
var is _playlist = req . body . is _playlist ;
try {
success = true ;
if ( ! is _playlist && type !== 'subscription' ) {
db . get ( ` files. ${ type } ` )
. find ( { uid : uid } )
. assign ( { sharingEnabled : true } )
. write ( ) ;
} else if ( is _playlist ) {
db . get ( ` playlists. ${ type } ` )
. find ( { id : uid } )
. assign ( { sharingEnabled : true } )
. write ( ) ;
} else if ( type === 'subscription' ) {
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
// time they are requested from the subscription directory.
} else {
// error
success = false ;
}
} catch ( err ) {
success = false ;
}
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/disableSharing' , function ( req , res ) {
var type = req . body . type ;
var uid = req . body . uid ;
var is _playlist = req . body . is _playlist ;
try {
success = true ;
if ( ! is _playlist && type !== 'subscription' ) {
db . get ( ` files. ${ type } ` )
. find ( { uid : uid } )
. assign ( { sharingEnabled : false } )
. write ( ) ;
} else if ( is _playlist ) {
db . get ( ` playlists. ${ type } ` )
. find ( { id : uid } )
. assign ( { sharingEnabled : false } )
. write ( ) ;
} else if ( type === 'subscription' ) {
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
// time they are requested from the subscription directory.
} else {
// error
success = false ;
}
} catch ( err ) {
success = false ;
}
res . send ( {
success : success
} ) ;
} ) ;
app . post ( '/api/subscribe' , async ( req , res ) => {
let name = req . body . name ;
let url = req . body . url ;
let timerange = req . body . timerange ;
let streamingOnly = req . body . streamingOnly ;
const new _sub = {
name : name ,
url : url ,
id : uuid ( ) ,
streamingOnly : streamingOnly
} ;
// adds timerange if it exists, otherwise all videos will be downloaded
if ( timerange ) {
new _sub . timerange = timerange ;
}
const result _obj = await subscriptions _api . subscribe ( new _sub ) ;
if ( result _obj . success ) {
res . send ( {
new _sub : new _sub
} ) ;
} else {
res . send ( {
new _sub : null ,
error : result _obj . error
} )
}
} ) ;
app . post ( '/api/unsubscribe' , async ( req , res ) => {
let deleteMode = req . body . deleteMode
let sub = req . body . sub ;
let result _obj = subscriptions _api . unsubscribe ( sub , deleteMode ) ;
if ( result _obj . success ) {
res . send ( {
success : result _obj . success
} ) ;
} else {
res . send ( {
success : false ,
error : result _obj . error
} ) ;
}
} ) ;
app . post ( '/api/deleteSubscriptionFile' , async ( req , res ) => {
let deleteForever = req . body . deleteForever ;
let file = req . body . file ;
let sub = req . body . sub ;
let success = await subscriptions _api . deleteSubscriptionFile ( sub , file , deleteForever ) ;
if ( success ) {
res . send ( {
success : success
} ) ;
} else {
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/api/getSubscription' , async ( req , res ) => {
let subID = req . body . id ;
// get sub from db
let subscription = subscriptions _api . getSubscription ( subID ) ;
if ( ! subscription ) {
// failed to get subscription from db, send 400 error
res . sendStatus ( 400 ) ;
return ;
}
// get sub videos
if ( subscription . name && ! subscription . streamingOnly ) {
let base _path = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
let appended _base _path = path . join ( base _path , subscription . isPlaylist ? 'playlists' : 'channels' , subscription . name , '/' ) ;
let files ;
try {
files = recFindByExt ( appended _base _path , 'mp4' ) ;
} catch ( e ) {
files = null ;
logger . info ( 'Failed to get folder for subscription: ' + subscription . name + ' at path ' + appended _base _path ) ;
res . sendStatus ( 500 ) ;
return ;
}
var parsed _files = [ ] ;
for ( let i = 0 ; i < files . length ; i ++ ) {
let file = files [ i ] ;
var file _path = file . substring ( appended _base _path . length , file . length ) ;
var stats = fs . statSync ( file ) ;
var id = file _path . substring ( 0 , file _path . length - 4 ) ;
var jsonobj = getJSONMp4 ( id , appended _base _path ) ;
if ( ! jsonobj ) continue ;
var title = jsonobj . title ;
var thumbnail = jsonobj . thumbnail ;
var duration = jsonobj . duration ;
var url = jsonobj . webpage _url ;
var uploader = jsonobj . uploader ;
var upload _date = jsonobj . upload _date ;
upload _date = ` ${ upload _date . substring ( 0 , 4 ) } - ${ upload _date . substring ( 4 , 6 ) } - ${ upload _date . substring ( 6 , 8 ) } ` ;
var size = stats . size ;
var isaudio = false ;
var file _obj = new File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file , upload _date ) ;
parsed _files . push ( file _obj ) ;
}
res . send ( {
subscription : subscription ,
files : parsed _files
} ) ;
} else if ( subscription . name && subscription . streamingOnly ) {
// return list of videos
let parsed _files = [ ] ;
if ( subscription . videos ) {
for ( let i = 0 ; i < subscription . videos . length ; i ++ ) {
const video = subscription . videos [ i ] ;
parsed _files . push ( new File ( video . title , video . title , video . thumbnail , false , video . duration , video . url , video . uploader , video . size , null , null , video . upload _date ) ) ;
}
}
res . send ( {
subscription : subscription ,
files : parsed _files
} ) ;
} else {
res . sendStatus ( 500 ) ;
}
} ) ;
app . post ( '/api/downloadVideosForSubscription' , async ( req , res ) => {
let subID = req . body . subID ;
let sub = subscriptions _api . getSubscription ( subID ) ;
subscriptions _api . getVideosForSub ( sub ) ;
res . send ( {
success : true
} ) ;
} ) ;
app . post ( '/api/getAllSubscriptions' , async ( req , res ) => {
// get subs from api
let subscriptions = subscriptions _api . getAllSubscriptions ( ) ;
res . send ( {
subscriptions : subscriptions
} ) ;
} ) ;
app . post ( '/api/createPlaylist' , async ( req , res ) => {
let playlistName = req . body . playlistName ;
let fileNames = req . body . fileNames ;
let type = req . body . type ;
let thumbnailURL = req . body . thumbnailURL ;
let new _playlist = {
'name' : playlistName ,
fileNames : fileNames ,
id : shortid . generate ( ) ,
thumbnailURL : thumbnailURL ,
type : type
} ;
db . get ( ` playlists. ${ type } ` )
. push ( new _playlist )
. write ( ) ;
res . send ( {
new _playlist : new _playlist ,
success : ! ! new _playlist // always going to be true
} )
} ) ;
app . post ( '/api/getPlaylist' , async ( req , res ) => {
let playlistID = req . body . playlistID ;
let type = req . body . type ;
let playlist = null ;
if ( ! type ) {
playlist = db . get ( 'playlists.audio' ) . find ( { id : playlistID } ) . value ( ) ;
if ( ! playlist ) {
playlist = db . get ( 'playlists.video' ) . find ( { id : playlistID } ) . value ( ) ;
if ( playlist ) type = 'video' ;
} else {
type = 'audio' ;
}
}
if ( ! playlist ) playlist = db . get ( ` playlists. ${ type } ` ) . find ( { id : playlistID } ) . value ( ) ;
res . send ( {
playlist : playlist ,
type : type ,
success : ! ! playlist
} ) ;
} ) ;
app . post ( '/api/updatePlaylist' , async ( req , res ) => {
let playlistID = req . body . playlistID ;
let fileNames = req . body . fileNames ;
let type = req . body . type ;
let success = false ;
try {
db . get ( ` playlists. ${ type } ` )
. find ( { id : playlistID } )
. assign ( { fileNames : fileNames } )
. write ( ) ;
/ * l o g g e r . i n f o ( ' s u c c e s s ! ' ) ;
let new _val = db . get ( ` playlists. ${ type } ` )
. find ( { id : playlistID } )
. value ( ) ;
logger . info ( new _val ) ; * /
success = true ;
} catch ( e ) {
logger . error ( ` Failed to find playlist with ID ${ playlistID } ` ) ;
}
res . send ( {
success : success
} )
} ) ;
app . post ( '/api/deletePlaylist' , async ( req , res ) => {
let playlistID = req . body . playlistID ;
let type = req . body . type ;
let success = null ;
try {
// removes playlist from playlists
db . get ( ` playlists. ${ type } ` )
. remove ( { id : playlistID } )
. write ( ) ;
success = true ;
} catch ( e ) {
success = false ;
}
res . send ( {
success : success
} )
} ) ;
// deletes mp3 file
app . post ( '/api/deleteMp3' , async ( req , res ) => {
// var name = req.body.name;
var uid = req . body . uid ;
var audio _obj = db . get ( 'files.audio' ) . find ( { uid : uid } ) . value ( ) ;
var name = audio _obj . id ;
var blacklistMode = req . body . blacklistMode ;
var fullpath = audioFolderPath + name + ".mp3" ;
var wasDeleted = false ;
if ( fs . existsSync ( fullpath ) )
{
deleteAudioFile ( name , blacklistMode ) ;
db . get ( 'files.audio' ) . remove ( { uid : uid } ) . write ( ) ;
wasDeleted = true ;
res . send ( wasDeleted ) ;
res . end ( "yes" ) ;
} else if ( audio _obj ) {
db . get ( 'files.audio' ) . remove ( { uid : uid } ) . write ( ) ;
wasDeleted = true ;
res . send ( wasDeleted ) ;
} else {
wasDeleted = false ;
res . send ( wasDeleted ) ;
}
} ) ;
// deletes mp4 file
app . post ( '/api/deleteMp4' , async ( req , res ) => {
var uid = req . body . uid ;
var video _obj = db . get ( 'files.video' ) . find ( { uid : uid } ) . value ( ) ;
var name = video _obj . id ;
var blacklistMode = req . body . blacklistMode ;
var fullpath = videoFolderPath + name + ".mp4" ;
var wasDeleted = false ;
if ( fs . existsSync ( fullpath ) )
{
wasDeleted = await deleteVideoFile ( name , null , blacklistMode ) ;
db . get ( 'files.video' ) . remove ( { uid : uid } ) . write ( ) ;
// wasDeleted = true;
res . send ( wasDeleted ) ;
res . end ( "yes" ) ;
} else if ( video _obj ) {
db . get ( 'files.video' ) . remove ( { uid : uid } ) . write ( ) ;
wasDeleted = true ;
res . send ( wasDeleted ) ;
} else {
wasDeleted = false ;
res . send ( wasDeleted ) ;
res . end ( "yes" ) ;
}
} ) ;
app . post ( '/api/downloadFile' , 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 . subscriptionPlaylist ;
let file = null ;
if ( ! zip _mode ) {
fileNames = decodeURIComponent ( fileNames ) ;
if ( type === 'audio' ) {
if ( ! subscriptionName ) {
file = path . join ( _ _dirname , audioFolderPath , fileNames + '.mp3' ) ;
} else {
let basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
file = path . join ( _ _dirname , basePath , ( subscriptionPlaylist ? 'playlists' : 'channels' ) , subscriptionName , fileNames + '.mp3' )
}
} else {
// if type is 'subscription' or 'video', it's a video
if ( ! subscriptionName ) {
file = path . join ( _ _dirname , videoFolderPath , fileNames + '.mp4' ) ;
} else {
let basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
file = path . join ( _ _dirname , basePath , ( subscriptionPlaylist ? 'playlists' : 'channels' ) , subscriptionName , fileNames + '.mp4' )
}
}
} else {
for ( let i = 0 ; i < fileNames . length ; i ++ ) {
fileNames [ i ] = decodeURIComponent ( fileNames [ i ] ) ;
}
file = await createPlaylistZipFile ( fileNames , type , outputName , fullPathProvided ) ;
}
res . sendFile ( file , function ( err ) {
if ( err ) {
logger . error ( err ) ;
} else if ( fullPathProvided ) {
try {
fs . unlinkSync ( file ) ;
} catch ( e ) {
logger . error ( "Failed to remove file" , file ) ;
}
}
} ) ;
} ) ;
app . post ( '/api/deleteFile' , async ( req , res ) => {
let fileName = req . body . fileName ;
let type = req . body . type ;
if ( type === 'audio' ) {
deleteAudioFile ( fileName ) ;
} else if ( type === 'video' ) {
deleteVideoFile ( fileName ) ;
}
res . send ( { } ) ;
} ) ;
app . post ( '/api/downloadArchive' , async ( req , res ) => {
let sub = req . body . sub ;
let archive _dir = sub . archive ;
let full _archive _path = path . join ( _ _dirname , archive _dir , 'archive.txt' ) ;
if ( fs . existsSync ( full _archive _path ) ) {
res . sendFile ( full _archive _path ) ;
} else {
res . sendStatus ( 404 ) ;
}
} ) ;
// Updater API calls
app . get ( '/api/updaterStatus' , async ( req , res ) => {
let status = updaterStatus ;
if ( status ) {
res . send ( updaterStatus ) ;
} else {
res . sendStatus ( 404 ) ;
}
} ) ;
app . post ( '/api/updateServer' , async ( req , res ) => {
let tag = req . body . tag ;
updateServer ( tag ) ;
res . send ( {
success : true
} ) ;
} ) ;
// Pin API calls
app . post ( '/api/isPinSet' , async ( req , res ) => {
let stored _pin = db . get ( 'pin_md5' ) . value ( ) ;
let is _set = false ;
if ( ! stored _pin || stored _pin . length === 0 ) {
} else {
is _set = true ;
}
res . send ( {
is _set : is _set
} ) ;
} ) ;
app . post ( '/api/setPin' , async ( req , res ) => {
let unhashed _pin = req . body . pin ;
let hashed _pin = md5 ( unhashed _pin ) ;
db . set ( 'pin_md5' , hashed _pin ) . write ( ) ;
res . send ( {
success : true
} ) ;
} ) ;
app . post ( '/api/checkPin' , async ( req , res ) => {
let input _pin = req . body . input _pin ;
let input _pin _md5 = md5 ( input _pin ) ;
let stored _pin = db . get ( 'pin_md5' ) . value ( ) ;
let successful = false ;
if ( input _pin _md5 === stored _pin ) {
successful = true ;
}
res . send ( {
success : successful
} ) ;
} ) ;
// API Key API calls
app . post ( '/api/generateNewAPIKey' , function ( req , res ) {
const new _api _key = uuid ( ) ;
config _api . setConfigItem ( 'ytdl_api_key' , new _api _key ) ;
res . send ( { new _api _key : new _api _key } ) ;
} ) ;
// Streaming API calls
app . get ( '/api/video/:id' , function ( req , res ) {
var head ;
let optionalParams = url _api . parse ( req . url , true ) . query ;
let id = decodeURIComponent ( req . params . id ) ;
let path = videoFolderPath + id + '.mp4' ;
if ( optionalParams [ 'subName' ] ) {
let basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
const isPlaylist = optionalParams [ 'subPlaylist' ] ;
basePath += ( isPlaylist === 'true' ? 'playlists/' : 'channels/' ) ;
path = basePath + optionalParams [ 'subName' ] + '/' + id + '.mp4' ;
}
const stat = fs . statSync ( 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 ( path , { start , end } )
if ( descriptors [ id ] ) descriptors [ id ] . push ( file ) ;
else descriptors [ id ] = [ file ] ;
file . on ( 'close' , function ( ) {
let index = descriptors [ id ] . indexOf ( file ) ;
descriptors [ id ] . splice ( index , 1 ) ;
logger . debug ( 'Successfully closed stream and removed file reference.' ) ;
} ) ;
head = {
'Content-Range' : ` bytes ${ start } - ${ end } / ${ fileSize } ` ,
'Accept-Ranges' : 'bytes' ,
'Content-Length' : chunksize ,
'Content-Type' : 'video/mp4' ,
}
res . writeHead ( 206 , head ) ;
file . pipe ( res ) ;
} else {
head = {
'Content-Length' : fileSize ,
'Content-Type' : 'video/mp4' ,
}
res . writeHead ( 200 , head )
fs . createReadStream ( path ) . pipe ( res )
}
} ) ;
app . get ( '/api/audio/:id' , function ( req , res ) {
var head ;
let id = decodeURIComponent ( req . params . id ) ;
let path = "audio/" + id + '.mp3' ;
path = path . replace ( /\"/g , '\'' ) ;
const stat = fs . statSync ( 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 ( path , { start , end } ) ;
if ( descriptors [ id ] ) descriptors [ id ] . push ( file ) ;
else descriptors [ id ] = [ file ] ;
file . on ( 'close' , function ( ) {
let index = descriptors [ id ] . indexOf ( file ) ;
descriptors [ id ] . splice ( index , 1 ) ;
logger . debug ( 'Successfully closed stream and removed file reference.' ) ;
} ) ;
head = {
'Content-Range' : ` bytes ${ start } - ${ end } / ${ fileSize } ` ,
'Accept-Ranges' : 'bytes' ,
'Content-Length' : chunksize ,
'Content-Type' : 'audio/mp3' ,
}
res . writeHead ( 206 , head ) ;
file . pipe ( res ) ;
} else {
head = {
'Content-Length' : fileSize ,
'Content-Type' : 'audio/mp3' ,
}
res . writeHead ( 200 , head )
fs . createReadStream ( path ) . pipe ( res )
}
} ) ;
// Downloads management
app . get ( '/api/downloads' , async ( req , res ) => {
/ *
if ( ! last _downloads _check || Date . now ( ) - last _downloads _check > downloads _check _interval ) {
last _downloads _check = Date . now ( ) ;
updateDownloads ( ) ;
}
* /
res . send ( { downloads : downloads } ) ;
} ) ;
app . post ( '/api/clearDownloads' , async ( req , res ) => {
let success = false ;
var delete _all = req . body . delete _all ;
if ( ! req . body . session _id ) req . body . session _id = 'undeclared' ;
var session _id = req . body . session _id ;
var download _id = req . body . download _id ;
if ( delete _all ) {
// delete all downloads
downloads = { } ;
success = true ;
} else if ( download _id ) {
// delete just 1 download
if ( downloads [ session _id ] [ download _id ] ) {
delete downloads [ session _id ] [ download _id ] ;
success = true ;
} else if ( ! downloads [ session _id ] ) {
logger . error ( ` Session ${ session _id } has no downloads. ` )
} else if ( ! downloads [ session _id ] [ download _id ] ) {
logger . error ( ` Download ' ${ download _id } ' for session ' ${ session _id } ' could not be found ` ) ;
}
} else if ( session _id ) {
// delete a session's downloads
if ( downloads [ session _id ] ) {
delete downloads [ session _id ] ;
success = true ;
} else {
logger . error ( ` Session ${ session _id } has no downloads. ` )
}
}
updateDownloads ( ) ;
res . send ( { success : success , downloads : downloads } ) ;
} ) ;
app . post ( '/api/getVideoInfos' , async ( req , res ) => {
let fileNames = req . body . fileNames ;
let urlMode = ! ! req . body . urlMode ;
let type = req . body . type ;
let result = null ;
if ( ! urlMode ) {
if ( type === 'audio' ) {
result = getAudioInfos ( fileNames )
} else if ( type === 'video' ) {
result = getVideoInfos ( fileNames ) ;
}
} else {
result = await getUrlInfos ( fileNames ) ;
}
res . send ( {
result : result ,
success : ! ! result
} )
} ) ;
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 ) ) ;