const fs = require ( 'fs-extra' )
const path = require ( 'path' )
const { v4 : uuid } = require ( 'uuid' ) ;
const config _api = require ( './config' ) ;
const db _api = require ( './db' ) ;
const archive _api = require ( './archive' ) ;
const utils = require ( './utils' )
const logger = require ( './logger' ) ;
exports . registerFileDB = async ( file _path , type , user _uid = null , category = null , sub _id = null , cropFileSettings = null , file _object = null ) => {
if ( ! file _object ) file _object = generateFileObject ( file _path , type ) ;
if ( ! file _object ) {
logger . error ( ` Could not find associated JSON file for ${ type } file ${ file _path } ` ) ;
return false ;
}
utils . fixVideoMetadataPerms ( file _path , type ) ;
// add thumbnail path
file _object [ 'thumbnailPath' ] = utils . getDownloadedThumbnail ( 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 ( user _uid ) file _object [ 'user_uid' ] = user _uid ;
if ( sub _id ) file _object [ 'sub_id' ] = sub _id ;
const file _obj = await registerFileDBManual ( file _object ) ;
// remove metadata JSON if needed
if ( ! config _api . getConfigItem ( 'ytdl_include_metadata' ) ) {
utils . deleteJSONFile ( file _path , type )
}
return file _obj ;
}
async function registerFileDBManual ( file _object ) {
// add additional info
file _object [ 'uid' ] = uuid ( ) ;
file _object [ 'registered' ] = Date . now ( ) ;
const path _object = path . parse ( file _object [ 'path' ] ) ;
file _object [ 'path' ] = path . format ( path _object ) ;
await db _api . insertRecordIntoTable ( 'files' , file _object , { path : file _object [ 'path' ] } )
return file _object ;
}
function generateFileObject ( file _path , type ) {
const jsonobj = utils . getJSON ( file _path , type ) ;
if ( ! jsonobj ) {
return null ;
} else if ( ! jsonobj [ '_filename' ] ) {
logger . error ( ` Failed to get filename from info JSON! File ${ jsonobj [ 'title' ] } could not be added. ` ) ;
return null ;
}
const true _file _path = utils . getTrueFileName ( jsonobj [ '_filename' ] , type ) ;
// console.
const stats = fs . statSync ( true _file _path ) ;
const file _id = utils . removeFileExtension ( path . basename ( file _path ) ) ;
const title = jsonobj . title ;
const url = jsonobj . webpage _url ;
const uploader = jsonobj . uploader ;
const upload _date = utils . formatDateString ( jsonobj . upload _date ) ;
const size = stats . size ;
const thumbnail = jsonobj . thumbnail ;
const duration = jsonobj . duration ;
const isaudio = type === 'audio' ;
const description = jsonobj . description ;
const file _obj = new utils . File ( file _id , title , thumbnail , isaudio , duration , url , uploader , size , true _file _path , upload _date , description , jsonobj . view _count , jsonobj . height , jsonobj . abr ) ;
return file _obj ;
}
exports . importUnregisteredFiles = async ( ) => {
const imported _files = [ ] ;
const dirs _to _check = await db _api . getFileDirectoriesAndDBs ( ) ;
// run through check list and check each file to see if it's missing from the db
for ( let i = 0 ; i < dirs _to _check . length ; i ++ ) {
const dir _to _check = dirs _to _check [ i ] ;
// recursively get all files in dir's path
const files = await utils . getDownloadedFilesByType ( dir _to _check . basePath , dir _to _check . type ) ;
for ( let j = 0 ; j < files . length ; j ++ ) {
const file = files [ j ] ;
// check if file exists in db, if not add it
const files _with _same _url = await db _api . getRecords ( 'files' , { url : file . url , sub _id : dir _to _check . sub _id } ) ;
const file _is _registered = ! ! ( files _with _same _url . find ( file _with _same _url => path . resolve ( file _with _same _url . path ) === path . resolve ( file . path ) ) ) ;
if ( ! file _is _registered ) {
// add additional info
const file _obj = await exports . registerFileDB ( file [ 'path' ] , dir _to _check . type , dir _to _check . user _uid , null , dir _to _check . sub _id , null ) ;
if ( file _obj ) {
imported _files . push ( file _obj [ 'uid' ] ) ;
logger . verbose ( ` Added discovered file to the database: ${ file . id } ` ) ;
} else {
logger . error ( ` Failed to import ${ file [ 'path' ] } automatically. ` ) ;
}
}
}
}
return imported _files ;
}
exports . addMetadataPropertyToDB = async ( property _key ) => {
try {
const dirs _to _check = await db _api . getFileDirectoriesAndDBs ( ) ;
const update _obj = { } ;
for ( let i = 0 ; i < dirs _to _check . length ; i ++ ) {
const dir _to _check = dirs _to _check [ i ] ;
// recursively get all files in dir's path
const files = await utils . getDownloadedFilesByType ( dir _to _check . basePath , dir _to _check . type , true ) ;
for ( let j = 0 ; j < files . length ; j ++ ) {
const file = files [ j ] ;
if ( file [ property _key ] ) {
update _obj [ file . uid ] = { [ property _key ] : file [ property _key ] } ;
}
}
}
return await db _api . bulkUpdateRecordsByKey ( 'files' , 'uid' , update _obj ) ;
} catch ( err ) {
logger . error ( err ) ;
return false ;
}
}
exports . createPlaylist = async ( playlist _name , uids , user _uid = null ) => {
const first _video = await exports . getVideo ( uids [ 0 ] ) ;
const thumbnailToUse = first _video [ 'thumbnailURL' ] ;
let new _playlist = {
name : playlist _name ,
uids : uids ,
id : uuid ( ) ,
thumbnailURL : thumbnailToUse ,
registered : Date . now ( ) ,
randomize _order : false
} ;
new _playlist . user _uid = user _uid ? user _uid : undefined ;
await db _api . insertRecordIntoTable ( 'playlists' , new _playlist ) ;
const duration = await exports . calculatePlaylistDuration ( new _playlist ) ;
await db _api . updateRecord ( 'playlists' , { id : new _playlist . id } , { duration : duration } ) ;
return new _playlist ;
}
exports . getPlaylist = async ( playlist _id , user _uid = null , require _sharing = false ) => {
let playlist = await db _api . getRecord ( 'playlists' , { id : playlist _id } ) ;
if ( ! playlist ) {
playlist = await db _api . getRecord ( 'categories' , { uid : playlist _id } ) ;
if ( playlist ) {
const uids = ( await db _api . getRecords ( 'files' , { 'category.uid' : playlist _id } ) ) . map ( file => file . uid ) ;
playlist [ 'uids' ] = uids ;
playlist [ 'auto' ] = true ;
}
}
// 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 = await 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 ) => {
let playlistID = playlist . id ;
const duration = await exports . calculatePlaylistDuration ( playlist ) ;
playlist . duration = duration ;
return await db _api . updateRecord ( 'playlists' , { id : playlistID } , playlist ) ;
}
exports . setPlaylistProperty = async ( playlist _id , assignment _obj , user _uid = null ) => {
let success = await db _api . updateRecord ( 'playlists' , { id : playlist _id } , assignment _obj ) ;
if ( ! success ) {
success = await db _api . updateRecord ( 'categories' , { uid : playlist _id } , assignment _obj ) ;
}
if ( ! success ) {
logger . error ( ` Could not find playlist or category with ID ${ playlist _id } ` ) ;
}
return success ;
}
exports . calculatePlaylistDuration = async ( playlist , 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 ) ;
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 , blacklistMode = false ) => {
const file _obj = await exports . getVideo ( uid ) ;
const type = file _obj . isAudio ? 'audio' : 'video' ;
const folderPath = path . dirname ( file _obj . path ) ;
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 || file _obj . sub _id ) {
// get id/extractor from JSON
const info _json = await ( type === 'audio' ? utils . getJSONMp3 ( name , folderPath ) : utils . getJSONMp4 ( name , folderPath ) ) ;
let retrievedID = null ;
let retrievedExtractor = null ;
if ( info _json ) {
retrievedID = info _json [ 'id' ] ;
retrievedExtractor = info _json [ 'extractor' ] ;
}
// Remove file ID from the archive file, and write it to the blacklist (if enabled)
if ( ! blacklistMode ) {
await archive _api . removeFromArchive ( retrievedExtractor , retrievedID , type , file _obj . user _uid , file _obj . sub _id )
} else {
const exists _in _archive = await archive _api . existsInArchive ( retrievedExtractor , retrievedID , type , file _obj . user _uid , file _obj . sub _id ) ;
if ( ! exists _in _archive ) {
await archive _api . addToArchive ( retrievedExtractor , retrievedID , type , file _obj . title , file _obj . user _uid , file _obj . sub _id ) ;
}
}
}
if ( jsonExists ) await fs . unlink ( jsonPath ) ;
if ( thumbnailExists ) await fs . unlink ( thumbnailPath ) ;
await db _api . removeRecord ( 'files' , { uid : uid } ) ;
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 = async ( file _id , uuid = null ) => {
const file _obj = await db _api . getRecord ( 'files' , { id : file _id } ) ;
return file _obj ? file _obj [ 'uid' ] : null ;
}
exports . getVideo = async ( file _uid ) => {
return await db _api . getRecord ( 'files' , { uid : file _uid } ) ;
}
exports . getAllFiles = async ( sort , range , text _search , file _type _filter , favorite _filter , sub _id , uuid ) => {
const filter _obj = { user _uid : uuid } ;
const regex = true ;
if ( text _search ) {
if ( regex ) {
filter _obj [ 'title' ] = { $regex : ` .* ${ text _search } .* ` , $options : 'i' } ;
} else {
filter _obj [ '$text' ] = { $search : utils . createEdgeNGrams ( text _search ) } ;
}
}
if ( favorite _filter ) {
filter _obj [ 'favorite' ] = true ;
}
if ( sub _id ) {
filter _obj [ 'sub_id' ] = sub _id ;
}
if ( file _type _filter === 'audio_only' ) filter _obj [ 'isAudio' ] = true ;
else if ( file _type _filter === 'video_only' ) filter _obj [ 'isAudio' ] = false ;
const files = JSON . parse ( JSON . stringify ( await db _api . getRecords ( 'files' , filter _obj , false , sort , range , text _search ) ) ) ;
const file _count = await db _api . getRecords ( 'files' , filter _obj , true ) ;
return { files , file _count } ;
}