const fs = require ( 'fs-extra' ) ;
const path = require ( 'path' ) ;
const youtubedl _api = require ( './youtube-dl' ) ;
const config _api = require ( './config' ) ;
const archive _api = require ( './archive' ) ;
const utils = require ( './utils' ) ;
const logger = require ( './logger' ) ;
const CONSTS = require ( './consts' ) ;
const debugMode = process . env . YTDL _MODE === 'debug' ;
const db _api = require ( './db' ) ;
const downloader _api = require ( './downloader' ) ;
exports . subscribe = async ( sub , user _uid = null , skip _get _info = false ) => {
const result _obj = {
success : false ,
error : ''
} ;
return new Promise ( async resolve => {
// sub should just have url and name. here we will get isPlaylist and path
sub . isPlaylist = sub . isPlaylist || sub . url . includes ( 'playlist' ) ;
sub . videos = [ ] ;
let url _exists = ! ! ( await db _api . getRecord ( 'subscriptions' , { url : sub . url , user _uid : user _uid } ) ) ;
if ( ! sub . name && url _exists ) {
logger . error ( ` Sub with the same URL " ${ sub . url } " already exists -- please provide a custom name for this new subscription. ` ) ;
result _obj . error = 'Subcription with URL ' + sub . url + ' already exists! Custom name is required.' ;
resolve ( result _obj ) ;
return ;
}
sub [ 'user_uid' ] = user _uid ? user _uid : undefined ;
await db _api . insertRecordIntoTable ( 'subscriptions' , JSON . parse ( JSON . stringify ( sub ) ) ) ;
let success = skip _get _info ? true : await getSubscriptionInfo ( sub ) ;
exports . writeSubscriptionMetadata ( sub ) ;
if ( success ) {
if ( ! sub . paused ) exports . getVideosForSub ( sub . id ) ;
} else {
logger . error ( 'Subscribe: Failed to get subscription info. Subscribe failed.' )
}
result _obj . success = success ;
result _obj . sub = sub ;
resolve ( result _obj ) ;
} ) ;
}
async function getSubscriptionInfo ( sub ) {
// get videos
let downloadConfig = [ '--dump-json' , '--playlist-end' , '1' ] ;
let useCookies = config _api . getConfigItem ( 'ytdl_use_cookies' ) ;
if ( useCookies ) {
if ( await fs . pathExists ( path . join ( _ _dirname , 'appdata' , 'cookies.txt' ) ) ) {
downloadConfig . push ( '--cookies' , path . join ( 'appdata' , 'cookies.txt' ) ) ;
} else {
logger . warn ( 'Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.' ) ;
}
}
let { callback } = await youtubedl _api . runYoutubeDL ( sub . url , downloadConfig ) ;
const { parsed _output , err } = await callback ;
if ( err ) {
logger . error ( err . stderr ) ;
return false ;
}
logger . verbose ( 'Subscribe: got info for subscription ' + sub . id ) ;
for ( const output _json of parsed _output ) {
if ( ! output _json ) {
continue ;
}
if ( ! sub . name ) {
if ( sub . isPlaylist ) {
sub . name = output _json . playlist _title ? output _json . playlist _title : output _json . playlist ;
} else {
sub . name = output _json . uploader ;
}
// if it's now valid, update
if ( sub . name ) {
let sub _name = sub . name ;
const sub _name _exists = await db _api . getRecord ( 'subscriptions' , { name : sub . name , isPlaylist : sub . isPlaylist , user _uid : sub . user _uid } ) ;
if ( sub _name _exists ) sub _name += ` - ${ sub . id } ` ;
await db _api . updateRecord ( 'subscriptions' , { id : sub . id } , { name : sub _name } ) ;
}
}
return true ;
}
return false ;
}
exports . unsubscribe = async ( sub _id , deleteMode , user _uid = null ) => {
const sub = await exports . getSubscription ( sub _id ) ;
let basePath = null ;
if ( user _uid )
basePath = path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , 'subscriptions' ) ;
else
basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
let id = sub . id ;
const sub _files = await db _api . getRecords ( 'files' , { sub _id : id } ) ;
for ( let i = 0 ; i < sub _files . length ; i ++ ) {
const sub _file = sub _files [ i ] ;
if ( config _api . descriptors [ sub _file [ 'uid' ] ] ) {
try {
for ( let i = 0 ; i < config _api . descriptors [ sub _file [ 'uid' ] ] . length ; i ++ ) {
config _api . descriptors [ sub _file [ 'uid' ] ] [ i ] . destroy ( ) ;
}
} catch ( e ) {
continue ;
}
}
}
await killSubDownloads ( sub _id , true ) ;
await db _api . removeRecord ( 'subscriptions' , { id : id } ) ;
await db _api . removeAllRecords ( 'files' , { sub _id : id } ) ;
// failed subs have no name, on unsubscribe they shouldn't error
if ( ! sub . name ) {
return ;
}
const appendedBasePath = getAppendedBasePath ( sub , basePath ) ;
if ( deleteMode && ( await fs . pathExists ( appendedBasePath ) ) ) {
await fs . remove ( appendedBasePath ) ;
}
await db _api . removeAllRecords ( 'archives' , { sub _id : sub . id } ) ;
}
exports . deleteSubscriptionFile = async ( sub , file , deleteForever , file _uid = null , user _uid = null ) => {
if ( typeof sub === 'string' ) {
// TODO: fix bad workaround where sub is a sub_id
sub = await db _api . getRecord ( 'subscriptions' , { sub _id : sub } ) ;
}
// TODO: combine this with deletefile
let basePath = null ;
basePath = user _uid ? path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , 'subscriptions' )
: config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
const appendedBasePath = getAppendedBasePath ( sub , basePath ) ;
const name = file ;
let retrievedID = null ;
let retrievedExtractor = null ;
await db _api . removeRecord ( 'files' , { uid : file _uid } ) ;
let filePath = appendedBasePath ;
const ext = ( sub . type && sub . type === 'audio' ) ? '.mp3' : '.mp4'
var jsonPath = path . join ( _ _dirname , filePath , name + '.info.json' ) ;
var videoFilePath = path . join ( _ _dirname , filePath , name + ext ) ;
var imageFilePath = path . join ( _ _dirname , filePath , name + '.jpg' ) ;
var altImageFilePath = path . join ( _ _dirname , filePath , name + '.webp' ) ;
const [ jsonExists , videoFileExists , imageFileExists , altImageFileExists ] = await Promise . all ( [
fs . pathExists ( jsonPath ) ,
fs . pathExists ( videoFilePath ) ,
fs . pathExists ( imageFilePath ) ,
fs . pathExists ( altImageFilePath ) ,
] ) ;
if ( jsonExists ) {
const info _json = fs . readJSONSync ( jsonPath ) ;
retrievedID = info _json [ 'id' ] ;
retrievedExtractor = info _json [ 'extractor' ] ;
await fs . unlink ( jsonPath ) ;
}
if ( imageFileExists ) {
await fs . unlink ( imageFilePath ) ;
}
if ( altImageFileExists ) {
await fs . unlink ( altImageFilePath ) ;
}
if ( videoFileExists ) {
await fs . unlink ( videoFilePath ) ;
if ( ( await fs . pathExists ( jsonPath ) ) || ( await fs . pathExists ( videoFilePath ) ) ) {
return false ;
} else {
// check if the user wants the video to be redownloaded (deleteForever === false)
if ( deleteForever ) {
// ensure video is in the archives
const exists _in _archive = await archive _api . existsInArchive ( retrievedExtractor , retrievedID , sub . type , user _uid , sub . id ) ;
if ( ! exists _in _archive ) {
await archive _api . addToArchive ( retrievedExtractor , retrievedID , sub . type , file . title , user _uid , sub . id ) ;
}
} else {
await archive _api . removeFromArchive ( retrievedExtractor , retrievedID , sub . type , user _uid , sub . id ) ;
}
return true ;
}
} else {
// TODO: tell user that the file didn't exist
return true ;
}
}
let current _sub _index = 0 ; // To keep track of the current subscription
exports . watchSubscriptionsInterval = async ( ) => {
const subscriptions _check _interval = config _api . getConfigItem ( 'ytdl_subscriptions_check_interval' ) ;
let parent _interval = setInterval ( ( ) => watchSubscriptions ( ) , subscriptions _check _interval * 1000 ) ;
watchSubscriptions ( ) ;
config _api . config _updated . subscribe ( change => {
if ( ! change ) return ;
if ( change [ 'key' ] === 'ytdl_subscriptions_check_interval' || change [ 'key' ] === 'ytdl_multi_user_mode' ) {
current _sub _index = 0 ; // TODO: start after the last sub check
logger . verbose ( 'Resetting sub check schedule due to config change' ) ;
clearInterval ( parent _interval ) ;
const new _interval = config _api . getConfigItem ( 'ytdl_subscriptions_check_interval' ) ;
parent _interval = setInterval ( ( ) => watchSubscriptions ( ) , new _interval * 1000 ) ;
watchSubscriptions ( ) ;
}
} ) ;
}
async function watchSubscriptions ( ) {
const subscription _ids = await getValidSubscriptionsToCheck ( ) ;
if ( subscription _ids . length === 0 ) {
logger . info ( 'Skipping subscription check as no valid subscriptions exist.' ) ;
return ;
}
checkSubscription ( subscription _ids [ current _sub _index ] ) ;
current _sub _index = ( current _sub _index + 1 ) % subscription _ids . length ;
}
async function checkSubscription ( sub _id ) {
let sub = await exports . getSubscription ( sub _id ) ;
// don't check the sub if the last check for the same subscription has not completed
if ( sub . downloading ) {
logger . verbose ( ` Subscription: skipped checking ${ sub . name } as it's downloading videos. ` ) ;
return ;
}
if ( ! sub . name ) {
logger . verbose ( ` Subscription: skipped check for subscription with uid ${ sub . id } as name has not been retrieved yet. ` ) ;
return ;
}
await exports . getVideosForSub ( sub . id ) ;
}
async function getValidSubscriptionsToCheck ( ) {
const subscriptions = await exports . getAllSubscriptions ( ) ;
if ( ! subscriptions ) return ;
// auto pause deprecated streamingOnly mode
const streaming _only _subs = subscriptions . filter ( sub => sub . streamingOnly ) ;
exports . updateSubscriptionPropertyMultiple ( streaming _only _subs , { paused : true } ) ;
const valid _subscription _ids = subscriptions . filter ( sub => ! sub . paused && ! sub . streamingOnly ) . map ( sub => sub . id ) ;
return valid _subscription _ids ;
}
exports . getVideosForSub = async ( sub _id ) => {
const sub = await exports . getSubscription ( sub _id ) ;
if ( ! sub || sub [ 'downloading' ] ) {
return false ;
}
_getVideosForSub ( sub ) ;
return true ;
}
async function _getVideosForSub ( sub ) {
const user _uid = sub [ 'user_uid' ] ;
updateSubscriptionProperty ( sub , { downloading : true } , user _uid ) ;
// get basePath
let basePath = null ;
if ( user _uid )
basePath = path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , 'subscriptions' ) ;
else
basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
let appendedBasePath = getAppendedBasePath ( sub , basePath ) ;
fs . ensureDirSync ( appendedBasePath ) ;
const downloadConfig = await generateArgsForSubscription ( sub , user _uid ) ;
// get videos
logger . verbose ( ` Subscription: getting list of videos to download for ${ sub . name } with args: ${ downloadConfig . join ( ',' ) } ` ) ;
let { child _process , callback } = await youtubedl _api . runYoutubeDL ( sub . url , downloadConfig ) ;
updateSubscriptionProperty ( sub , { child _process : child _process } , user _uid ) ;
const { parsed _output , err } = await callback ;
updateSubscriptionProperty ( sub , { downloading : false , child _process : null } , user _uid ) ;
if ( ! parsed _output ) {
logger . error ( 'Subscription check failed!' ) ;
if ( err ) logger . error ( err ) ;
return null ;
}
// remove temporary archive file if it exists
const archive _path = path . join ( appendedBasePath , 'archive.txt' ) ;
const archive _exists = await fs . pathExists ( archive _path ) ;
if ( archive _exists ) {
await fs . unlink ( archive _path ) ;
}
logger . verbose ( 'Subscription: finished check for ' + sub . name ) ;
const files _to _download = await handleOutputJSON ( parsed _output , sub , user _uid ) ;
return files _to _download ;
}
async function handleOutputJSON ( output _jsons , sub , user _uid ) {
if ( config _api . getConfigItem ( 'ytdl_subscriptions_redownload_fresh_uploads' ) ) {
await setFreshUploads ( sub , user _uid ) ;
checkVideosForFreshUploads ( sub , user _uid ) ;
}
if ( output _jsons . length === 0 || ( output _jsons . length === 1 && output _jsons [ 0 ] === '' ) ) {
logger . verbose ( 'No additional videos to download for ' + sub . name ) ;
return [ ] ;
}
const files _to _download = await getFilesToDownload ( sub , output _jsons ) ;
const base _download _options = exports . generateOptionsForSubscriptionDownload ( sub , user _uid ) ;
for ( let j = 0 ; j < files _to _download . length ; j ++ ) {
const file _to _download = files _to _download [ j ] ;
file _to _download [ 'formats' ] = utils . stripPropertiesFromObject ( file _to _download [ 'formats' ] , [ 'format_id' , 'filesize' , 'filesize_approx' ] ) ; // prevent download object from blowing up in size
await downloader _api . createDownload ( file _to _download [ 'webpage_url' ] , sub . type || 'video' , base _download _options , user _uid , sub . id , sub . name , [ file _to _download ] ) ;
}
return files _to _download ;
}
exports . generateOptionsForSubscriptionDownload = ( sub , user _uid ) => {
let basePath = null ;
if ( user _uid )
basePath = path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , 'subscriptions' ) ;
else
basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
let default _output = config _api . getConfigItem ( 'ytdl_default_file_output' ) ? config _api . getConfigItem ( 'ytdl_default_file_output' ) : '%(title)s' ;
const base _download _options = {
maxHeight : sub . maxQuality && sub . maxQuality !== 'best' ? sub . maxQuality : null ,
customFileFolderPath : getAppendedBasePath ( sub , basePath ) ,
customOutput : sub . custom _output ? ` ${ sub . custom _output } ` : ` ${ default _output } ` ,
customArchivePath : path . join ( basePath , 'archives' , sub . name ) ,
additionalArgs : sub . custom _args
}
return base _download _options ;
}
async function generateArgsForSubscription ( sub , user _uid , redownload = false , desired _path = null ) {
// get basePath
let basePath = null ;
if ( user _uid )
basePath = path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , user _uid , 'subscriptions' ) ;
else
basePath = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
let appendedBasePath = getAppendedBasePath ( sub , basePath ) ;
const file _output = config _api . getConfigItem ( 'ytdl_default_file_output' ) ? config _api . getConfigItem ( 'ytdl_default_file_output' ) : '%(title)s' ;
let fullOutput = ` " ${ appendedBasePath } / ${ file _output } .%(ext)s" ` ;
if ( desired _path ) {
fullOutput = ` " ${ desired _path } .%(ext)s" ` ;
} else if ( sub . custom _output ) {
fullOutput = ` " ${ appendedBasePath } / ${ sub . custom _output } .%(ext)s" ` ;
}
let downloadConfig = [ '--dump-json' , '-o' , fullOutput , ! redownload ? '-ciw' : '-ci' , '--write-info-json' , '--print-json' ] ;
let qualityPath = null ;
if ( sub . type && sub . type === 'audio' ) {
qualityPath = [ '-f' , 'bestaudio' ]
qualityPath . push ( '-x' ) ;
qualityPath . push ( '--audio-format' , 'mp3' ) ;
} else {
if ( ! sub . maxQuality || sub . maxQuality === 'best' ) qualityPath = [ '-f' , 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4' ] ;
else qualityPath = [ '-f' , ` bestvideo[height<= ${ sub . maxQuality } ]+bestaudio/best[height<= ${ sub . maxQuality } ] ` , '--merge-output-format' , 'mp4' ] ;
}
downloadConfig . push ( ... qualityPath )
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
const archive _text = await archive _api . generateArchive ( sub . type , sub . user _uid , sub . id ) ;
const archive _count = archive _text . split ( '\n' ) . length - 1 ;
if ( archive _count > 0 ) {
logger . verbose ( ` Generating temporary archive file for subscription ${ sub . name } with ${ archive _count } entries. ` )
const archive _path = path . join ( appendedBasePath , 'archive.txt' ) ;
await fs . writeFile ( archive _path , archive _text ) ;
downloadConfig . push ( '--download-archive' , archive _path ) ;
}
if ( sub . custom _args ) {
const customArgsArray = sub . custom _args . split ( ',,' ) ;
if ( customArgsArray . indexOf ( '-f' ) !== - 1 ) {
// if custom args has a custom quality, replce the original quality with that of custom args
const original _output _index = downloadConfig . indexOf ( '-f' ) ;
downloadConfig . splice ( original _output _index , 2 ) ;
}
downloadConfig . push ( ... customArgsArray ) ;
}
if ( sub . timerange && ! redownload ) {
downloadConfig . push ( '--dateafter' , sub . timerange ) ;
}
let useCookies = config _api . getConfigItem ( 'ytdl_use_cookies' ) ;
if ( useCookies ) {
if ( await fs . pathExists ( path . join ( _ _dirname , 'appdata' , 'cookies.txt' ) ) ) {
downloadConfig . push ( '--cookies' , path . join ( 'appdata' , 'cookies.txt' ) ) ;
} else {
logger . warn ( 'Cookies file could not be found. You can either upload one, or disable \'use cookies\' in the Advanced tab in the settings.' ) ;
}
}
if ( config _api . getConfigItem ( 'ytdl_include_thumbnail' ) ) {
downloadConfig . push ( '--write-thumbnail' ) ;
}
const rate _limit = config _api . getConfigItem ( 'ytdl_download_rate_limit' ) ;
if ( rate _limit && downloadConfig . indexOf ( '-r' ) === - 1 && downloadConfig . indexOf ( '--limit-rate' ) === - 1 ) {
downloadConfig . push ( '-r' , rate _limit ) ;
}
const default _downloader = config _api . getConfigItem ( 'ytdl_default_downloader' ) ;
if ( default _downloader === 'yt-dlp' ) {
downloadConfig . push ( '--no-clean-info-json' ) ;
}
downloadConfig = utils . filterArgs ( downloadConfig , [ '--write-comments' ] ) ;
return downloadConfig ;
}
async function getFilesToDownload ( sub , output _jsons ) {
const files _to _download = [ ] ;
for ( let i = 0 ; i < output _jsons . length ; i ++ ) {
const output _json = output _jsons [ i ] ;
const file _missing = ! ( await db _api . getRecord ( 'files' , { sub _id : sub . id , url : output _json [ 'webpage_url' ] } ) ) && ! ( await db _api . getRecord ( 'download_queue' , { sub _id : sub . id , url : output _json [ 'webpage_url' ] , error : null , finished : false } ) ) ;
if ( file _missing ) {
const file _with _path _exists = await db _api . getRecord ( 'files' , { sub _id : sub . id , path : output _json [ '_filename' ] } ) ;
if ( file _with _path _exists ) {
// or maybe just overwrite???
logger . info ( ` Skipping adding file ${ output _json [ '_filename' ] } for subscription ${ sub . name } as a file with that path already exists. ` )
continue ;
}
const exists _in _archive = await archive _api . existsInArchive ( output _json [ 'extractor' ] , output _json [ 'id' ] , sub . type , sub . user _uid , sub . id ) ;
if ( exists _in _archive ) continue ;
files _to _download . push ( output _json ) ;
}
}
return files _to _download ;
}
exports . cancelCheckSubscription = async ( sub _id ) => {
const sub = await exports . getSubscription ( sub _id ) ;
if ( ! sub [ 'downloading' ] && ! sub [ 'child_process' ] ) {
logger . error ( 'Failed to cancel subscription check, verify that it is still running!' ) ;
return false ;
}
// if check is ongoing
if ( sub [ 'child_process' ] ) {
const child _process = sub [ 'child_process' ] ;
youtubedl _api . killYoutubeDLProcess ( child _process ) ;
}
// cancel activate video downloads
await killSubDownloads ( sub _id ) ;
return true ;
}
async function killSubDownloads ( sub _id , remove _downloads = false ) {
const sub _downloads = await db _api . getRecords ( 'download_queue' , { sub _id : sub _id } ) ;
for ( const sub _download of sub _downloads ) {
if ( sub _download [ 'running' ] )
await downloader _api . cancelDownload ( sub _download [ 'uid' ] ) ;
if ( remove _downloads )
await db _api . removeRecord ( 'download_queue' , { uid : sub _download [ 'uid' ] } ) ;
}
}
exports . getSubscriptions = async ( user _uid = null ) => {
// TODO: fix issue where the downloading property may not match getSubscription()
return await db _api . getRecords ( 'subscriptions' , { user _uid : user _uid } ) ;
}
exports . getAllSubscriptions = async ( ) => {
const all _subs = await db _api . getRecords ( 'subscriptions' ) ;
const multiUserMode = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) ;
return all _subs . filter ( sub => ! ! ( sub . user _uid ) === ! ! multiUserMode ) ;
}
exports . getSubscription = async ( subID ) => {
// stringify and parse because we may override the 'downloading' property
const sub = JSON . parse ( JSON . stringify ( await db _api . getRecord ( 'subscriptions' , { id : subID } ) ) ) ;
// now with the download_queue, we may need to override 'downloading'
const current _downloads = await db _api . getRecords ( 'download_queue' , { running : true , sub _id : subID } , true ) ;
if ( ! sub [ 'downloading' ] ) sub [ 'downloading' ] = current _downloads > 0 ;
return sub ;
}
exports . getSubscriptionByName = async ( subName , user _uid = null ) => {
return await db _api . getRecord ( 'subscriptions' , { name : subName , user _uid : user _uid } ) ;
}
exports . updateSubscription = async ( sub ) => {
await db _api . updateRecord ( 'subscriptions' , { id : sub . id } , sub ) ;
exports . writeSubscriptionMetadata ( sub ) ;
return true ;
}
exports . updateSubscriptionPropertyMultiple = async ( subs , assignment _obj ) => {
subs . forEach ( async sub => {
await updateSubscriptionProperty ( sub , assignment _obj ) ;
} ) ;
}
async function updateSubscriptionProperty ( sub , assignment _obj ) {
// TODO: combine with updateSubscription
await db _api . updateRecord ( 'subscriptions' , { id : sub . id } , assignment _obj ) ;
return true ;
}
exports . writeSubscriptionMetadata = ( sub ) => {
let basePath = sub . user _uid ? path . join ( config _api . getConfigItem ( 'ytdl_users_base_path' ) , sub . user _uid , 'subscriptions' )
: config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ;
const appendedBasePath = getAppendedBasePath ( sub , basePath ) ;
const metadata _path = path . join ( appendedBasePath , CONSTS . SUBSCRIPTION _BACKUP _PATH ) ;
fs . ensureDirSync ( appendedBasePath ) ;
fs . writeJSONSync ( metadata _path , sub ) ;
}
async function setFreshUploads ( sub ) {
const sub _files = await db _api . getRecords ( 'files' , { sub _id : sub . id } ) ;
if ( ! sub _files ) return ;
const current _date = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] . replace ( /-/g , '' ) ;
sub _files . forEach ( async file => {
if ( current _date === file [ 'upload_date' ] . replace ( /-/g , '' ) ) {
// set upload as fresh
const file _uid = file [ 'uid' ] ;
await db _api . setVideoProperty ( file _uid , { 'fresh_upload' : true } ) ;
}
} ) ;
}
async function checkVideosForFreshUploads ( sub , user _uid ) {
const sub _files = await db _api . getRecords ( 'files' , { sub _id : sub . id } ) ;
const current _date = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] . replace ( /-/g , '' ) ;
sub _files . forEach ( async file => {
if ( file [ 'fresh_upload' ] && current _date > file [ 'upload_date' ] . replace ( /-/g , '' ) ) {
await checkVideoIfBetterExists ( file , sub , user _uid )
}
} ) ;
}
async function checkVideoIfBetterExists ( file _obj , sub , user _uid ) {
const new _path = file _obj [ 'path' ] . substring ( 0 , file _obj [ 'path' ] . length - 4 ) ;
const downloadConfig = await generateArgsForSubscription ( sub , user _uid , true , new _path ) ;
logger . verbose ( ` Checking if a better version of the fresh upload ${ file _obj [ 'id' ] } exists. ` ) ;
// simulate a download to verify that a better version exists
const info = await downloader _api . getVideoInfoByURL ( file _obj [ 'url' ] , downloadConfig ) ;
if ( info && info . length === 1 ) {
const metric _to _compare = sub . type === 'audio' ? 'abr' : 'height' ;
if ( info [ metric _to _compare ] > file _obj [ metric _to _compare ] ) {
// download new video as the simulated one is better
let { callback } = await youtubedl _api . runYoutubeDL ( sub . url , downloadConfig ) ;
const { parsed _output , err } = await callback ;
if ( err ) {
logger . verbose ( ` Failed to download better version of video ${ file _obj [ 'id' ] } ` ) ;
} else if ( parsed _output ) {
logger . verbose ( ` Successfully upgraded video ${ file _obj [ 'id' ] } 's ${ metric _to _compare } from ${ file _obj [ metric _to _compare ] } to ${ info [ metric _to _compare ] } ` ) ;
await db _api . setVideoProperty ( file _obj [ 'uid' ] , { [ metric _to _compare ] : info [ metric _to _compare ] } ) ;
}
}
}
await db _api . setVideoProperty ( file _obj [ 'uid' ] , { 'fresh_upload' : false } ) ;
}
// helper functions
function getAppendedBasePath ( sub , base _path ) {
return path . join ( base _path , ( sub . isPlaylist ? 'playlists/' : 'channels/' ) , sub . name ) ;
}