var fs = require ( 'fs-extra' )
var path = require ( 'path' )
var utils = require ( './utils' )
const { uuid } = require ( 'uuidv4' ) ;
const config _api = require ( './config' ) ;
var logger = null ;
var db = null ;
var users _db = null ;
function setDB ( input _db , input _users _db ) { db = input _db ; users _db = input _users _db }
function setLogger ( input _logger ) { logger = input _logger ; }
exports . initialize = ( input _db , input _users _db , input _logger ) => {
setDB ( input _db , input _users _db ) ;
setLogger ( input _logger ) ;
}
exports . registerFileDB = ( file _path , type , multiUserMode = null , sub = null , customPath = null , category = null , cropFileSettings = null ) => {
let db _path = null ;
const file _id = utils . removeFileExtension ( file _path ) ;
const file _object = generateFileObject ( file _id , type , customPath || multiUserMode && multiUserMode . file _path , sub ) ;
if ( ! file _object ) {
logger . error ( ` Could not find associated JSON file for ${ type } file ${ file _id } ` ) ;
return false ;
}
utils . fixVideoMetadataPerms ( file _id , type , multiUserMode && multiUserMode . file _path ) ;
// add thumbnail path
file _object [ 'thumbnailPath' ] = utils . getDownloadedThumbnail ( file _id , type , customPath || multiUserMode && multiUserMode . file _path ) ;
// if category exists, only include essential info
if ( category ) file _object [ 'category' ] = { name : category [ 'name' ] , uid : category [ 'uid' ] } ;
// modify duration
if ( cropFileSettings ) {
file _object [ 'duration' ] = ( cropFileSettings . cropFileEnd || file _object . duration ) - cropFileSettings . cropFileStart ;
}
if ( ! sub ) {
if ( multiUserMode ) {
const user _uid = multiUserMode . user ;
db _path = users _db . get ( 'users' ) . find ( { uid : user _uid } ) . get ( ` files ` ) ;
} else {
db _path = db . get ( ` files ` ) ;
}
} else {
if ( multiUserMode ) {
const user _uid = multiUserMode . user ;
db _path = users _db . get ( 'users' ) . find ( { uid : user _uid } ) . get ( 'subscriptions' ) . find ( { id : sub . id } ) . get ( 'videos' ) ;
} else {
db _path = db . get ( 'subscriptions' ) . find ( { id : sub . id } ) . get ( 'videos' ) ;
}
}
const file _obj = registerFileDBManual ( db _path , file _object ) ;
// remove metadata JSON if needed
if ( ! config _api . getConfigItem ( 'ytdl_include_metadata' ) ) {
utils . deleteJSONFile ( file _id , type , multiUserMode && multiUserMode . file _path )
}
return file _obj ;
}
function registerFileDBManual ( db _path , file _object ) {
// add additional info
file _object [ 'uid' ] = uuid ( ) ;
file _object [ 'registered' ] = Date . now ( ) ;
path _object = path . parse ( file _object [ 'path' ] ) ;
file _object [ 'path' ] = path . format ( path _object ) ;
// remove duplicate(s)
db _path . remove ( { path : file _object [ 'path' ] } ) . write ( ) ;
// add new file to db
db _path . push ( file _object ) . write ( ) ;
return file _object ;
}
function generateFileObject ( id , type , customPath = null , sub = null ) {
if ( ! customPath && sub ) {
customPath = getAppendedBasePathSub ( sub , config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ) ;
}
var jsonobj = ( type === 'audio' ) ? utils . getJSONMp3 ( id , customPath , true ) : utils . getJSONMp4 ( id , customPath , true ) ;
if ( ! jsonobj ) {
return null ;
}
const ext = ( type === 'audio' ) ? '.mp3' : '.mp4'
const file _path = utils . getTrueFileName ( jsonobj [ '_filename' ] , type ) ; // path.join(type === 'audio' ? audioFolderPath : videoFolderPath, id + ext);
// console.
var stats = fs . statSync ( path . join ( _ _dirname , file _path ) ) ;
var title = jsonobj . title ;
var url = jsonobj . webpage _url ;
var uploader = jsonobj . uploader ;
var upload _date = jsonobj . upload _date ;
upload _date = upload _date ? ` ${ upload _date . substring ( 0 , 4 ) } - ${ upload _date . substring ( 4 , 6 ) } - ${ upload _date . substring ( 6 , 8 ) } ` : 'N/A' ;
var size = stats . size ;
var thumbnail = jsonobj . thumbnail ;
var duration = jsonobj . duration ;
var isaudio = type === 'audio' ;
var description = jsonobj . description ;
var file _obj = new utils . File ( id , title , thumbnail , isaudio , duration , url , uploader , size , file _path , upload _date , description , jsonobj . view _count , jsonobj . height , jsonobj . abr ) ;
return file _obj ;
}
function getAppendedBasePathSub ( sub , base _path ) {
return path . join ( base _path , ( sub . isPlaylist ? 'playlists/' : 'channels/' ) , sub . name ) ;
}
exports . getFileDirectoriesAndDBs = ( ) => {
let dirs _to _check = [ ] ;
let subscriptions _to _check = [ ] ;
const subscriptions _base _path = config _api . getConfigItem ( 'ytdl_subscriptions_base_path' ) ; // only for single-user mode
const multi _user _mode = config _api . getConfigItem ( 'ytdl_multi_user_mode' ) ;
const usersFileFolder = config _api . getConfigItem ( 'ytdl_users_base_path' ) ;
const subscriptions _enabled = config _api . getConfigItem ( 'ytdl_allow_subscriptions' ) ;
if ( multi _user _mode ) {
let users = users _db . get ( 'users' ) . value ( ) ;
for ( let i = 0 ; i < users . length ; i ++ ) {
const user = users [ i ] ;
if ( subscriptions _enabled ) subscriptions _to _check = subscriptions _to _check . concat ( users [ i ] [ 'subscriptions' ] ) ;
// add user's audio dir to check list
dirs _to _check . push ( {
basePath : path . join ( usersFileFolder , user . uid , 'audio' ) ,
dbPath : users _db . get ( 'users' ) . find ( { uid : user . uid } ) . get ( 'files' ) ,
type : 'audio'
} ) ;
// add user's video dir to check list
dirs _to _check . push ( {
basePath : path . join ( usersFileFolder , user . uid , 'video' ) ,
dbPath : users _db . get ( 'users' ) . find ( { uid : user . uid } ) . get ( 'files' ) ,
type : 'video'
} ) ;
}
} else {
const audioFolderPath = config _api . getConfigItem ( 'ytdl_audio_folder_path' ) ;
const videoFolderPath = config _api . getConfigItem ( 'ytdl_video_folder_path' ) ;
const subscriptions = db . get ( 'subscriptions' ) . value ( ) ;
if ( subscriptions _enabled && subscriptions ) subscriptions _to _check = subscriptions _to _check . concat ( subscriptions ) ;
// add audio dir to check list
dirs _to _check . push ( {
basePath : audioFolderPath ,
dbPath : db . get ( 'files' ) ,
type : 'audio'
} ) ;
// add video dir to check list
dirs _to _check . push ( {
basePath : videoFolderPath ,
dbPath : db . get ( 'files' ) ,
type : 'video'
} ) ;
}
// add subscriptions to check list
for ( let i = 0 ; i < subscriptions _to _check . length ; i ++ ) {
let subscription _to _check = subscriptions _to _check [ i ] ;
if ( ! subscription _to _check . name ) {
// TODO: Remove subscription as it'll never complete
continue ;
}
dirs _to _check . push ( {
basePath : multi _user _mode ? path . join ( usersFileFolder , subscription _to _check . user _uid , 'subscriptions' , subscription _to _check . isPlaylist ? 'playlists/' : 'channels/' , subscription _to _check . name )
: path . join ( subscriptions _base _path , subscription _to _check . isPlaylist ? 'playlists/' : 'channels/' , subscription _to _check . name ) ,
dbPath : multi _user _mode ? users _db . get ( 'users' ) . find ( { uid : subscription _to _check . user _uid } ) . get ( 'subscriptions' ) . find ( { id : subscription _to _check . id } ) . get ( 'videos' )
: db . get ( 'subscriptions' ) . find ( { id : subscription _to _check . id } ) . get ( 'videos' ) ,
type : subscription _to _check . type
} ) ;
}
return dirs _to _check ;
}
exports . importUnregisteredFiles = async ( ) => {
const dirs _to _check = exports . getFileDirectoriesAndDBs ( ) ;
// run through check list and check each file to see if it's missing from the db
for ( const dir _to _check of dirs _to _check ) {
// recursively get all files in dir's path
const files = await utils . getDownloadedFilesByType ( dir _to _check . basePath , dir _to _check . type ) ;
files . forEach ( file => {
// check if file exists in db, if not add it
const file _is _registered = ! ! ( dir _to _check . dbPath . find ( { id : file . id } ) . value ( ) )
if ( ! file _is _registered ) {
// add additional info
registerFileDBManual ( dir _to _check . dbPath , file ) ;
logger . verbose ( ` Added discovered file to the database: ${ file . id } ` ) ;
}
} ) ;
}
}
exports . preimportUnregisteredSubscriptionFile = async ( sub , appendedBasePath ) => {
const preimported _file _paths = [ ] ;
let dbPath = null ;
if ( sub . user _uid )
dbPath = users _db . get ( 'users' ) . find ( { uid : sub . user _uid } ) . get ( 'subscriptions' ) . find ( { id : sub . id } ) . get ( 'videos' ) ;
else
dbPath = db . get ( 'subscriptions' ) . find ( { id : sub . id } ) . get ( 'videos' ) ;
const files = await utils . getDownloadedFilesByType ( appendedBasePath , sub . type ) ;
files . forEach ( file => {
// check if file exists in db, if not add it
const file _is _registered = ! ! ( dbPath . find ( { id : file . id } ) . value ( ) )
if ( ! file _is _registered ) {
// add additional info
registerFileDBManual ( dbPath , file ) ;
preimported _file _paths . push ( file [ 'path' ] ) ;
logger . verbose ( ` Preemptively added subscription file to the database: ${ file . id } ` ) ;
}
} ) ;
return preimported _file _paths ;
}
exports . createPlaylist = async ( playlist _name , uids , type , thumbnail _url , user _uid = null ) => {
let new _playlist = {
name : playlist _name ,
uids : uids ,
id : uuid ( ) ,
thumbnailURL : thumbnail _url ,
type : type ,
registered : Date . now ( ) ,
} ;
const duration = await exports . calculatePlaylistDuration ( new _playlist , user _uid ) ;
new _playlist . duration = duration ;
if ( user _uid ) {
users _db . get ( 'users' ) . find ( { uid : user _uid } ) . get ( ` playlists ` ) . push ( new _playlist ) . write ( ) ;
} else {
db . get ( ` playlists ` )
. push ( new _playlist )
. write ( ) ;
}
return new _playlist ;
}
exports . getPlaylist = async ( playlist _id , user _uid = null , require _sharing = false ) => {
let playlist = null
if ( user _uid ) {
playlist = users _db . get ( 'users' ) . find ( { uid : user _uid } ) . get ( ` playlists ` ) . find ( { id : playlist _id } ) . value ( ) ;
} else {
playlist = db . get ( ` playlists ` ) . find ( { id : playlist _id } ) . value ( ) ;
}
if ( ! playlist ) {
playlist = db . get ( 'categories' ) . find ( { uid : playlist _id } ) . value ( ) ;
if ( playlist ) {
// category found
const files = await exports . getFiles ( user _uid ) ;
utils . addUIDsToCategory ( playlist , files ) ;
}
}
// converts playlists to new UID-based schema
if ( playlist && playlist [ 'fileNames' ] && ! playlist [ 'uids' ] ) {
playlist [ 'uids' ] = [ ] ;
logger . verbose ( ` Converting playlist ${ playlist [ 'name' ] } to new UID-based schema. ` ) ;
for ( let i = 0 ; i < playlist [ 'fileNames' ] . length ; i ++ ) {
const fileName = playlist [ 'fileNames' ] [ i ] ;
const uid = exports . getVideoUIDByID ( fileName , user _uid ) ;
if ( uid ) playlist [ 'uids' ] . push ( uid ) ;
else logger . warn ( ` Failed to convert file with name ${ fileName } to its UID while converting playlist ${ playlist [ 'name' ] } to the new UID-based schema. The original file is likely missing/deleted and it will be skipped. ` ) ;
}
exports . updatePlaylist ( playlist , user _uid ) ;
}
// prevent unauthorized users from accessing the file info
if ( require _sharing && ! playlist [ 'sharingEnabled' ] ) return null ;
return playlist ;
}
exports . updatePlaylist = async ( playlist , user _uid = null ) => {
let playlistID = playlist . id ;
const duration = await exports . calculatePlaylistDuration ( playlist , user _uid ) ;
playlist . duration = duration ;
let db _loc = null ;
if ( user _uid ) {
db _loc = users _db . get ( 'users' ) . find ( { uid : user _uid } ) . get ( ` playlists ` ) . find ( { id : playlistID } ) ;
} else {
db _loc = db . get ( ` playlists ` ) . find ( { id : playlistID } ) ;
}
db _loc . assign ( playlist ) . write ( ) ;
return true ;
}
exports . calculatePlaylistDuration = async ( playlist , uuid , playlist _file _objs = null ) => {
if ( ! playlist _file _objs ) {
playlist _file _objs = [ ] ;
for ( let i = 0 ; i < playlist [ 'uids' ] . length ; i ++ ) {
const uid = playlist [ 'uids' ] [ i ] ;
const file _obj = await exports . getVideo ( uid , uuid ) ;
if ( file _obj ) playlist _file _objs . push ( file _obj ) ;
}
}
return playlist _file _objs . reduce ( ( a , b ) => a + utils . durationStringToNumber ( b . duration ) , 0 ) ;
}
exports . deleteFile = async ( uid , uuid = null , blacklistMode = false ) => {
const file _obj = await exports . getVideo ( uid , uuid ) ;
const type = file _obj . isAudio ? 'audio' : 'video' ;
const folderPath = path . dirname ( file _obj . path ) ;
const ext = type === 'audio' ? 'mp3' : 'mp4' ;
const name = file _obj . id ;
const filePathNoExtension = utils . removeFileExtension ( file _obj . path ) ;
var jsonPath = ` ${ file _obj . path } .info.json ` ;
var altJSONPath = ` ${ filePathNoExtension } .info.json ` ;
var thumbnailPath = ` ${ filePathNoExtension } .webp ` ;
var altThumbnailPath = ` ${ filePathNoExtension } .jpg ` ;
jsonPath = path . join ( _ _dirname , jsonPath ) ;
altJSONPath = path . join ( _ _dirname , altJSONPath ) ;
let jsonExists = await fs . pathExists ( jsonPath ) ;
let thumbnailExists = await fs . pathExists ( thumbnailPath ) ;
if ( ! jsonExists ) {
if ( await fs . pathExists ( altJSONPath ) ) {
jsonExists = true ;
jsonPath = altJSONPath ;
}
}
if ( ! thumbnailExists ) {
if ( await fs . pathExists ( altThumbnailPath ) ) {
thumbnailExists = true ;
thumbnailPath = altThumbnailPath ;
}
}
let fileExists = await fs . pathExists ( file _obj . path ) ;
if ( config _api . descriptors [ uid ] ) {
try {
for ( let i = 0 ; i < config _api . descriptors [ uid ] . length ; i ++ ) {
config _api . descriptors [ uid ] [ i ] . destroy ( ) ;
}
} catch ( e ) {
}
}
let useYoutubeDLArchive = config _api . getConfigItem ( 'ytdl_use_youtubedl_archive' ) ;
if ( useYoutubeDLArchive ) {
const archive _path = uuid ? path . join ( usersFileFolder , uuid , 'archives' , ` archive_ ${ type } .txt ` ) : path . join ( 'appdata' , 'archives' , ` archive_ ${ type } .txt ` ) ;
// get ID from JSON
var jsonobj = await ( type === 'audio' ? utils . getJSONMp3 ( name , folderPath ) : utils . getJSONMp4 ( name , folderPath ) ) ;
let id = null ;
if ( jsonobj ) id = jsonobj . id ;
// use subscriptions API to remove video from the archive file, and write it to the blacklist
if ( await fs . pathExists ( archive _path ) ) {
const line = id ? await utils . removeIDFromArchive ( archive _path , id ) : null ;
if ( blacklistMode && line ) await writeToBlacklist ( type , line ) ;
} else {
logger . info ( 'Could not find archive file for audio files. Creating...' ) ;
await fs . close ( await fs . open ( archive _path , 'w' ) ) ;
}
}
if ( jsonExists ) await fs . unlink ( jsonPath ) ;
if ( thumbnailExists ) await fs . unlink ( thumbnailPath ) ;
const base _db _path = uuid ? users _db . get ( 'users' ) . find ( { uid : uuid } ) : db ;
base _db _path . get ( 'files' ) . remove ( { uid : uid } ) . write ( ) ;
if ( fileExists ) {
await fs . unlink ( file _obj . path ) ;
if ( await fs . pathExists ( jsonPath ) || await fs . pathExists ( file _obj . path ) ) {
return false ;
} else {
return true ;
}
} else {
// TODO: tell user that the file didn't exist
return true ;
}
}
// Video ID is basically just the file name without the base path and file extension - this method helps us get away from that
exports . getVideoUIDByID = ( file _id , uuid = null ) => {
const base _db _path = uuid ? users _db . get ( 'users' ) . find ( { uid : uuid } ) : db ;
const file _obj = base _db _path . get ( 'files' ) . find ( { id : file _id } ) . value ( ) ;
return file _obj ? file _obj [ 'uid' ] : null ;
}
exports . getVideo = async ( file _uid , uuid = null , sub _id = null ) => {
const base _db _path = uuid ? users _db . get ( 'users' ) . find ( { uid : uuid } ) : db ;
const sub _db _path = sub _id ? base _db _path . get ( 'subscriptions' ) . find ( { id : sub _id } ) . get ( 'videos' ) : base _db _path . get ( 'files' ) ;
return sub _db _path . find ( { uid : file _uid } ) . value ( ) ;
}
exports . getFiles = async ( uuid = null ) => {
const base _db _path = uuid ? users _db . get ( 'users' ) . find ( { uid : uuid } ) : db ;
return base _db _path . get ( 'files' ) . value ( ) ;
}
exports . setVideoProperty = async ( file _uid , assignment _obj , uuid , sub _id ) => {
const base _db _path = uuid ? users _db . get ( 'users' ) . find ( { uid : uuid } ) : db ;
const sub _db _path = sub _id ? base _db _path . get ( 'subscriptions' ) . find ( { id : sub _id } ) . get ( 'videos' ) : base _db _path . get ( 'files' ) ;
const file _db _path = sub _db _path . find ( { uid : file _uid } ) ;
if ( ! ( file _db _path . value ( ) ) ) {
logger . error ( ` Failed to find file with uid ${ file _uid } ` ) ;
}
sub _db _path . find ( { uid : file _uid } ) . assign ( assignment _obj ) . write ( ) ;
}