mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			304 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			304 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Path = require('path')
 | |
| const njodb = require("njodb")
 | |
| const fs = require('fs-extra')
 | |
| const jwt = require('jsonwebtoken')
 | |
| const Logger = require('./Logger')
 | |
| const Audiobook = require('./objects/Audiobook')
 | |
| const User = require('./objects/User')
 | |
| const UserCollection = require('./objects/UserCollection')
 | |
| const Library = require('./objects/Library')
 | |
| const ServerSettings = require('./objects/ServerSettings')
 | |
| 
 | |
| class Db {
 | |
|   constructor(ConfigPath, AudiobookPath) {
 | |
|     this.ConfigPath = ConfigPath
 | |
|     this.AudiobookPath = AudiobookPath
 | |
|     this.AudiobooksPath = Path.join(ConfigPath, 'audiobooks')
 | |
|     this.UsersPath = Path.join(ConfigPath, 'users')
 | |
|     this.LibrariesPath = Path.join(ConfigPath, 'libraries')
 | |
|     this.SettingsPath = Path.join(ConfigPath, 'settings')
 | |
|     this.CollectionsPath = Path.join(ConfigPath, 'collections')
 | |
| 
 | |
|     this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
 | |
|     this.usersDb = new njodb.Database(this.UsersPath)
 | |
|     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
 | |
|     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
 | |
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
 | |
| 
 | |
|     this.users = []
 | |
|     this.libraries = []
 | |
|     this.audiobooks = []
 | |
|     this.settings = []
 | |
|     this.collections = []
 | |
| 
 | |
|     this.serverSettings = null
 | |
|   }
 | |
| 
 | |
|   getEntityDb(entityName) {
 | |
|     if (entityName === 'user') return this.usersDb
 | |
|     else if (entityName === 'audiobook') return this.audiobooksDb
 | |
|     else if (entityName === 'library') return this.librariesDb
 | |
|     else if (entityName === 'settings') return this.settingsDb
 | |
|     else if (entityName === 'collection') return this.collectionsDb
 | |
|     return null
 | |
|   }
 | |
| 
 | |
|   getEntityArrayKey(entityName) {
 | |
|     if (entityName === 'user') return 'users'
 | |
|     else if (entityName === 'audiobook') return 'audiobooks'
 | |
|     else if (entityName === 'library') return 'libraries'
 | |
|     else if (entityName === 'settings') return 'settings'
 | |
|     else if (entityName === 'collection') return 'collections'
 | |
|     return null
 | |
|   }
 | |
| 
 | |
|   getDefaultUser(token) {
 | |
|     return new User({
 | |
|       id: 'root',
 | |
|       type: 'root',
 | |
|       username: 'root',
 | |
|       pash: '',
 | |
|       stream: null,
 | |
|       token,
 | |
|       isActive: true,
 | |
|       createdAt: Date.now()
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   getDefaultLibrary() {
 | |
|     var defaultLibrary = new Library()
 | |
|     defaultLibrary.setData({
 | |
|       id: 'main',
 | |
|       name: 'Main',
 | |
|       folder: { // Generates default folder
 | |
|         id: 'audiobooks',
 | |
|         fullPath: this.AudiobookPath,
 | |
|         libraryId: 'main'
 | |
|       }
 | |
|     })
 | |
|     return defaultLibrary
 | |
|   }
 | |
| 
 | |
|   reinit() {
 | |
|     this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
 | |
|     this.usersDb = new njodb.Database(this.UsersPath)
 | |
|     this.librariesDb = new njodb.Database(this.LibrariesPath, { datastores: 2 })
 | |
|     this.settingsDb = new njodb.Database(this.SettingsPath, { datastores: 2 })
 | |
|     this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
 | |
|     return this.init()
 | |
|   }
 | |
| 
 | |
|   async init() {
 | |
|     await this.load()
 | |
| 
 | |
|     // Insert Defaults
 | |
|     var rootUser = this.users.find(u => u.type === 'root')
 | |
|     if (!rootUser) {
 | |
|       var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
 | |
|       Logger.debug('Generated default token', token)
 | |
|       Logger.info('[Db] Root user created')
 | |
|       await this.insertEntity('user', this.getDefaultUser(token))
 | |
|     } else {
 | |
|       Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
 | |
|     }
 | |
| 
 | |
|     if (!this.libraries.length) {
 | |
|       await this.insertEntity('library', this.getDefaultLibrary())
 | |
|     }
 | |
| 
 | |
|     if (!this.serverSettings) {
 | |
|       this.serverSettings = new ServerSettings()
 | |
|       await this.insertEntity('settings', this.serverSettings)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async load() {
 | |
|     var p1 = this.audiobooksDb.select(() => true).then((results) => {
 | |
|       this.audiobooks = results.data.map(a => new Audiobook(a))
 | |
|       Logger.info(`[DB] ${this.audiobooks.length} Audiobooks Loaded`)
 | |
|     })
 | |
|     var p2 = this.usersDb.select(() => true).then((results) => {
 | |
|       this.users = results.data.map(u => new User(u))
 | |
|       Logger.info(`[DB] ${this.users.length} Users Loaded`)
 | |
|     })
 | |
|     var p3 = this.librariesDb.select(() => true).then((results) => {
 | |
|       this.libraries = results.data.map(l => new Library(l))
 | |
|       Logger.info(`[DB] ${this.libraries.length} Libraries Loaded`)
 | |
|     })
 | |
|     var p4 = this.settingsDb.select(() => true).then((results) => {
 | |
|       if (results.data && results.data.length) {
 | |
|         this.settings = results.data
 | |
|         var serverSettings = this.settings.find(s => s.id === 'server-settings')
 | |
|         if (serverSettings) {
 | |
|           this.serverSettings = new ServerSettings(serverSettings)
 | |
|         }
 | |
|       }
 | |
|     })
 | |
|     var p5 = this.collectionsDb.select(() => true).then((results) => {
 | |
|       this.collections = results.data.map(l => new UserCollection(l))
 | |
|       Logger.info(`[DB] ${this.collections.length} Collections Loaded`)
 | |
|     })
 | |
|     await Promise.all([p1, p2, p3, p4, p5])
 | |
|   }
 | |
| 
 | |
|   updateAudiobook(audiobook) {
 | |
|     return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
 | |
|       Logger.debug(`[DB] Audiobook updated ${results.updated}`)
 | |
|       return true
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Audiobook update failed ${error}`)
 | |
|       return false
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   updateUserStream(userId, streamId) {
 | |
|     return this.usersDb.update((record) => record.id === userId, (user) => {
 | |
|       user.stream = streamId
 | |
|       return user
 | |
|     }).then((results) => {
 | |
|       Logger.debug(`[DB] Updated user ${results.updated}`)
 | |
|       this.users = this.users.map(u => {
 | |
|         if (u.id === userId) {
 | |
|           u.stream = streamId
 | |
|         }
 | |
|         return u
 | |
|       })
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Update user Failed ${error}`)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   insertEntity(entityName, entity) {
 | |
|     var entityDb = this.getEntityDb(entityName)
 | |
|     return entityDb.insert([entity]).then((results) => {
 | |
|       Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
 | |
| 
 | |
|       var arrayKey = this.getEntityArrayKey(entityName)
 | |
|       this[arrayKey].push(entity)
 | |
|       return true
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Failed to insert ${entityName}`, error)
 | |
|       return false
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   updateEntity(entityName, entity) {
 | |
|     var entityDb = this.getEntityDb(entityName)
 | |
| 
 | |
|     var jsonEntity = entity
 | |
|     if (entity && entity.toJSON) {
 | |
|       jsonEntity = entity.toJSON()
 | |
|     } else {
 | |
|       console.log('Entity has no json', jsonEntity)
 | |
|     }
 | |
| 
 | |
|     return entityDb.update((record) => record.id === entity.id, () => jsonEntity).then((results) => {
 | |
|       Logger.debug(`[DB] Updated entity ${entityName}: ${results.updated}`)
 | |
|       var arrayKey = this.getEntityArrayKey(entityName)
 | |
|       this[arrayKey] = this[arrayKey].map(e => {
 | |
|         return e.id === entity.id ? entity : e
 | |
|       })
 | |
|       return true
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`)
 | |
| 
 | |
|       if (error && error.code === 'ENOENT') {
 | |
|         this.attemptDataRecovery(entityName)
 | |
|       }
 | |
| 
 | |
|       return false
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   removeEntity(entityName, entityId) {
 | |
|     var entityDb = this.getEntityDb(entityName)
 | |
|     return entityDb.delete((record) => record.id === entityId).then((results) => {
 | |
|       Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`)
 | |
|       var arrayKey = this.getEntityArrayKey(entityName)
 | |
|       this[arrayKey] = this[arrayKey].filter(e => {
 | |
|         return e.id !== entityId
 | |
|       })
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Remove entity ${entityName} Failed: ${error}`)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   async attemptDataRecovery(entityName) {
 | |
|     var dbDirName = this.getEntityArrayKey(entityName)
 | |
|     var dbdir = Path.join(this.ConfigPath, dbDirName)
 | |
|     console.log('Attempting data recovery for:', dbdir)
 | |
| 
 | |
|     var exists = await fs.pathExists(dbdir)
 | |
|     if (!exists) {
 | |
|       console.error('Db dir does not exist', dbdir)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       var dbdatadir = Path.join(dbdir, 'data')
 | |
|       var dbtmpdir = Path.join(dbdir, 'tmp')
 | |
| 
 | |
|       var datafiles = await fs.readdir(dbdatadir)
 | |
|       var tempfiles = await fs.readdir(dbtmpdir)
 | |
| 
 | |
|       var orphanOld = datafiles.find(df => df.endsWith('.old'))
 | |
|       if (orphanOld) {
 | |
|         // Get data file num
 | |
|         var dbnum = orphanOld.split('.')[1]
 | |
|         console.log('Found orphan json.old', orphanOld, `Num: ${dbnum}`)
 | |
| 
 | |
|         var dbDataFilename = `data.${dbnum}.json`
 | |
| 
 | |
|         // make sure data.#.json does not already exist
 | |
|         if (datafiles.includes(dbDataFilename)) {
 | |
|           console.warn(`${dbDataFilename} already exists, not recovering`)
 | |
|           return
 | |
|         }
 | |
| 
 | |
|         // find temp file that was supposed to be renamed
 | |
|         var matchingTmp = tempfiles.find(tmp => tmp.startsWith(`data.${dbnum}`))
 | |
|         if (matchingTmp) {
 | |
|           console.log('found matching tmp file', matchingTmp)
 | |
| 
 | |
|           var tmpfileFullPath = Path.join(dbtmpdir, matchingTmp)
 | |
|           var renameToPath = Path.join(dbdatadir, dbDataFilename)
 | |
| 
 | |
|           console.log(`Renamining "${tmpfileFullPath}" => "${renameToPath}"`)
 | |
|           await fs.rename(tmpfileFullPath, renameToPath)
 | |
| 
 | |
|           console.log('Data recovery successful -- unlinking old')
 | |
| 
 | |
|           var orphanOldPath = Path.join(dbdatadir, orphanOld)
 | |
|           await fs.unlink(orphanOldPath)
 | |
|           console.log('Removed .old file')
 | |
| 
 | |
|           // Removing lock dir throws error in proper-lockfile
 | |
|           // var lockdirpath = Path.join(dbdatadir, `data.${dbnum}.json.lock`)
 | |
|           // var lockdirexists = await fs.pathExists(lockdirpath)
 | |
|           // if (lockdirexists) {
 | |
|           //   await fs.rmdir(lockdirpath)
 | |
|           //   console.log('Removed lock dir')
 | |
|           // } else {
 | |
|           //   console.log('No lock dir found', lockdirpath)
 | |
|           // }
 | |
|         }
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error('Data recovery failed', error)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   recreateAudiobookDb() {
 | |
|     return this.audiobooksDb.drop().then((results) => {
 | |
|       Logger.info(`[DB] Dropped audiobook db`, results)
 | |
|       this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
 | |
|       this.audiobooks = []
 | |
|       return true
 | |
|     }).catch((error) => {
 | |
|       Logger.error(`[DB] Failed to drop audiobook db`, error)
 | |
|       return false
 | |
|     })
 | |
|   }
 | |
| }
 | |
| module.exports = Db
 |