mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-27 11:41:36 +00:00
merge upstream
This commit is contained in:
commit
61bf30e08a
89 changed files with 2963 additions and 1805 deletions
|
|
@ -18,9 +18,10 @@ const AuthorFinder = require('./AuthorFinder')
|
|||
const FileSystemController = require('./controllers/FileSystemController')
|
||||
|
||||
class ApiController {
|
||||
constructor(MetadataPath, db, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
||||
constructor(db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
this.auth = auth
|
||||
this.scanner = scanner
|
||||
this.streamManager = streamManager
|
||||
this.rssFeeds = rssFeeds
|
||||
this.downloadManager = downloadManager
|
||||
|
|
@ -30,10 +31,9 @@ class ApiController {
|
|||
this.cacheManager = cacheManager
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
this.MetadataPath = MetadataPath
|
||||
|
||||
this.bookFinder = new BookFinder()
|
||||
this.authorFinder = new AuthorFinder(this.MetadataPath)
|
||||
this.authorFinder = new AuthorFinder()
|
||||
|
||||
this.router = express()
|
||||
this.init()
|
||||
|
|
@ -59,6 +59,7 @@ class ApiController {
|
|||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||
this.router.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.bind(this))
|
||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||
|
||||
// TEMP: Support old syntax for mobile app
|
||||
|
|
@ -82,6 +83,7 @@ class ApiController {
|
|||
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
|
||||
this.router.get('/books/:id/cover', BookController.getCover.bind(this))
|
||||
this.router.patch('/books/:id/coverfile', BookController.updateCoverFromFile.bind(this))
|
||||
this.router.post('/books/:id/match', BookController.match.bind(this))
|
||||
|
||||
// TEMP: Support old syntax for mobile app
|
||||
this.router.get('/audiobooks', BookController.findAll.bind(this)) // Old route should pass library id
|
||||
|
|
@ -176,6 +178,8 @@ class ApiController {
|
|||
|
||||
this.router.post('/syncStream', this.syncStream.bind(this))
|
||||
this.router.post('/syncLocal', this.syncLocal.bind(this))
|
||||
|
||||
this.router.post('/streams/:id/close', this.closeStream.bind(this))
|
||||
}
|
||||
|
||||
async findBooks(req, res) {
|
||||
|
|
@ -283,7 +287,7 @@ class ApiController {
|
|||
this.backupManager.updateCronSchedule()
|
||||
}
|
||||
|
||||
await this.db.updateEntity('settings', this.db.serverSettings)
|
||||
await this.db.updateServerSettings()
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
|
|
@ -416,6 +420,7 @@ class ApiController {
|
|||
data: audiobookProgress || null
|
||||
})
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -537,5 +542,12 @@ class ApiController {
|
|||
await this.cacheManager.purgeAll()
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async closeStream(req, res) {
|
||||
const streamId = req.params.id
|
||||
const userId = req.user.id
|
||||
this.streamManager.closeStreamApiRequest(userId, streamId)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
module.exports = ApiController
|
||||
|
|
@ -30,7 +30,7 @@ class Auth {
|
|||
cors(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Range, Authorization")
|
||||
res.header('Access-Control-Allow-Credentials', true)
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ const Audnexus = require('./providers/Audnexus')
|
|||
const { downloadFile } = require('./utils/fileUtils')
|
||||
|
||||
class AuthorFinder {
|
||||
constructor(MetadataPath) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.AuthorPath = Path.join(MetadataPath, 'authors')
|
||||
constructor() {
|
||||
this.AuthorPath = Path.join(global.MetadataPath, 'authors')
|
||||
|
||||
this.audnexus = new Audnexus()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ const Logger = require('./Logger')
|
|||
const Backup = require('./objects/Backup')
|
||||
|
||||
class BackupManager {
|
||||
constructor(MetadataPath, Uid, Gid, db) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.BackupPath = Path.join(this.MetadataPath, 'backups')
|
||||
constructor(Uid, Gid, db) {
|
||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
||||
|
||||
this.Uid = Uid
|
||||
this.Gid = Gid
|
||||
|
|
@ -142,10 +142,9 @@ class BackupManager {
|
|||
return
|
||||
}
|
||||
const zip = new StreamZip.async({ file: backup.fullPath })
|
||||
await zip.extract('config/', this.db.ConfigPath)
|
||||
await zip.extract('config/', global.ConfigPath)
|
||||
if (backup.backupMetadataCovers) {
|
||||
var metadataBooksPath = Path.join(this.MetadataPath, 'books')
|
||||
await zip.extract('metadata-books/', metadataBooksPath)
|
||||
await zip.extract('metadata-books/', this.MetadataBooksPath)
|
||||
}
|
||||
await this.db.reinit()
|
||||
socket.emit('apply_backup_complete', true)
|
||||
|
|
@ -157,7 +156,7 @@ class BackupManager {
|
|||
var lastBackup = this.backups.shift()
|
||||
|
||||
const zip = new StreamZip.async({ file: lastBackup.fullPath })
|
||||
await zip.extract('config/', this.db.ConfigPath)
|
||||
await zip.extract('config/', global.ConfigPath)
|
||||
console.log('Set Last Backup')
|
||||
await this.db.reinit()
|
||||
}
|
||||
|
|
@ -196,7 +195,7 @@ class BackupManager {
|
|||
async runBackup() {
|
||||
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
|
||||
Logger.info(`[BackupManager] Running Backup`)
|
||||
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
|
||||
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? this.MetadataBooksPath : null
|
||||
|
||||
var newBackup = new Backup()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ const Logger = require('./Logger')
|
|||
const { resizeImage } = require('./utils/ffmpegHelpers')
|
||||
|
||||
class CacheManager {
|
||||
constructor(MetadataPath) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.CachePath = Path.join(this.MetadataPath, 'cache')
|
||||
constructor() {
|
||||
this.CachePath = Path.join(global.MetadataPath, 'cache')
|
||||
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,21 +6,18 @@ const readChunk = require('read-chunk')
|
|||
const imageType = require('image-type')
|
||||
|
||||
const globals = require('./utils/globals')
|
||||
const { CoverDestination } = require('./utils/constants')
|
||||
const { downloadFile } = require('./utils/fileUtils')
|
||||
|
||||
class CoverController {
|
||||
constructor(db, cacheManager, MetadataPath, AudiobookPath) {
|
||||
constructor(db, cacheManager) {
|
||||
this.db = db
|
||||
this.cacheManager = cacheManager
|
||||
|
||||
this.MetadataPath = MetadataPath.replace(/\\/g, '/')
|
||||
this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books')
|
||||
this.AudiobookPath = AudiobookPath
|
||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||
}
|
||||
|
||||
getCoverDirectory(audiobook) {
|
||||
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
|
||||
if (this.db.serverSettings.storeCoverWithBook) {
|
||||
return {
|
||||
fullPath: audiobook.fullPath,
|
||||
relPath: '/s/book/' + audiobook.id
|
||||
|
|
|
|||
70
server/Db.js
70
server/Db.js
|
|
@ -13,18 +13,15 @@ const ServerSettings = require('./objects/ServerSettings')
|
|||
const SSOSettings = require('./objects/SSOSettings')
|
||||
|
||||
class Db {
|
||||
constructor(ConfigPath, AudiobookPath) {
|
||||
this.ConfigPath = ConfigPath
|
||||
this.AudiobookPath = AudiobookPath
|
||||
|
||||
this.AudiobooksPath = Path.join(ConfigPath, 'audiobooks')
|
||||
this.UsersPath = Path.join(ConfigPath, 'users')
|
||||
this.SessionsPath = Path.join(ConfigPath, 'sessions')
|
||||
this.LibrariesPath = Path.join(ConfigPath, 'libraries')
|
||||
this.SettingsPath = Path.join(ConfigPath, 'settings')
|
||||
this.SSOPath = Path.join(ConfigPath, 'sso')
|
||||
this.CollectionsPath = Path.join(ConfigPath, 'collections')
|
||||
this.AuthorsPath = Path.join(ConfigPath, 'authors')
|
||||
constructor() {
|
||||
this.AudiobooksPath = Path.join(global.ConfigPath, 'audiobooks')
|
||||
this.UsersPath = Path.join(global.ConfigPath, 'users')
|
||||
this.SessionsPath = Path.join(global.ConfigPath, 'sessions')
|
||||
this.LibrariesPath = Path.join(global.ConfigPath, 'libraries')
|
||||
this.SettingsPath = Path.join(global.ConfigPath, 'settings')
|
||||
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
|
||||
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
|
||||
this.SSOPath = Path.join(global.ConfigPath, 'sso')
|
||||
|
||||
this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
|
||||
this.usersDb = new njodb.Database(this.UsersPath)
|
||||
|
|
@ -91,7 +88,7 @@ class Db {
|
|||
name: 'Main',
|
||||
folder: { // Generates default folder
|
||||
id: 'audiobooks',
|
||||
fullPath: this.AudiobookPath,
|
||||
fullPath: global.AudiobookPath,
|
||||
libraryId: 'main'
|
||||
}
|
||||
})
|
||||
|
|
@ -136,6 +133,7 @@ class Db {
|
|||
this.SSOSettings = new SSOSettings()
|
||||
await this.insertEntity('settings', this.SSOSettings)
|
||||
}
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
|
@ -184,11 +182,19 @@ class Db {
|
|||
// Update server version in server settings
|
||||
if (this.previousVersion) {
|
||||
this.serverSettings.version = version
|
||||
await this.updateEntity('settings', this.serverSettings)
|
||||
await this.updateServerSettings()
|
||||
}
|
||||
}
|
||||
|
||||
updateAudiobook(audiobook) {
|
||||
async updateAudiobook(audiobook) {
|
||||
if (audiobook && audiobook.saveAbMetadata) {
|
||||
// TODO: Book may have updates where this save is not necessary
|
||||
// add check first if metadata update is needed
|
||||
await audiobook.saveAbMetadata()
|
||||
} else {
|
||||
Logger.error(`[Db] Invalid audiobook object passed to updateAudiobook`, audiobook)
|
||||
}
|
||||
|
||||
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
|
||||
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
||||
return true
|
||||
|
|
@ -198,6 +204,28 @@ class Db {
|
|||
})
|
||||
}
|
||||
|
||||
insertAudiobook(audiobook) {
|
||||
return this.insertAudiobooks([audiobook])
|
||||
}
|
||||
|
||||
async insertAudiobooks(audiobooks) {
|
||||
// TODO: Books may have updates where this save is not necessary
|
||||
// add check first if metadata update is needed
|
||||
await Promise.all(audiobooks.map(async (ab) => {
|
||||
if (ab && ab.saveAbMetadata) return ab.saveAbMetadata()
|
||||
return null
|
||||
}))
|
||||
|
||||
return this.audiobooksDb.insert(audiobooks).then((results) => {
|
||||
Logger.debug(`[DB] Audiobooks inserted ${results.inserted}`)
|
||||
this.audiobooks = this.audiobooks.concat(audiobooks)
|
||||
return true
|
||||
}).catch((error) => {
|
||||
Logger.error(`[DB] Audiobooks insert failed ${error}`)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
updateUserStream(userId, streamId) {
|
||||
return this.usersDb.update((record) => record.id === userId, (user) => {
|
||||
user.stream = streamId
|
||||
|
|
@ -215,6 +243,11 @@ class Db {
|
|||
})
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
return this.updateEntity('settings', this.serverSettings)
|
||||
}
|
||||
|
||||
insertEntities(entityName, entities) {
|
||||
var entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.insert(entities).then((results) => {
|
||||
|
|
@ -322,5 +355,12 @@ class Db {
|
|||
return []
|
||||
})
|
||||
}
|
||||
|
||||
// Check if server was updated and previous version was earlier than param
|
||||
checkPreviousVersionIsBefore(version) {
|
||||
if (!this.previousVersion) return false
|
||||
// true if version > previousVersion
|
||||
return version.localeCompare(this.previousVersion) >= 0
|
||||
}
|
||||
}
|
||||
module.exports = Db
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
|
|||
const { getFileSize } = require('./utils/fileUtils')
|
||||
const TAG = 'DownloadManager'
|
||||
class DownloadManager {
|
||||
constructor(db, MetadataPath, AudiobookPath, Uid, Gid) {
|
||||
constructor(db, Uid, Gid) {
|
||||
this.Uid = Uid
|
||||
this.Gid = Gid
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
this.AudiobookPath = AudiobookPath
|
||||
|
||||
this.downloadDirPath = Path.join(this.MetadataPath, 'downloads')
|
||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||
|
||||
this.pendingDownloads = []
|
||||
this.downloads = []
|
||||
|
|
@ -248,7 +246,7 @@ class DownloadManager {
|
|||
// Supporting old local file prefix
|
||||
var bookCoverPath = audiobook.book.cover ? audiobook.book.cover.replace(/\\/g, '/') : null
|
||||
if (!_cover && bookCoverPath && bookCoverPath.startsWith('/local')) {
|
||||
_cover = Path.posix.join(this.AudiobookPath.replace(/\\/g, '/'), _cover.replace('/local', ''))
|
||||
_cover = Path.posix.join(global.AudiobookPath, _cover.replace('/local', ''))
|
||||
Logger.debug('Local cover url', _cover)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ const fs = require('fs-extra')
|
|||
const Logger = require('./Logger')
|
||||
|
||||
class HlsController {
|
||||
constructor(db, auth, streamManager, emitter, StreamsPath) {
|
||||
constructor(db, auth, streamManager, emitter) {
|
||||
this.db = db
|
||||
this.auth = auth
|
||||
this.streamManager = streamManager
|
||||
this.emitter = emitter
|
||||
this.StreamsPath = StreamsPath
|
||||
|
||||
this.router = express()
|
||||
this.init()
|
||||
|
|
@ -27,7 +26,7 @@ class HlsController {
|
|||
|
||||
async streamFileRequest(req, res) {
|
||||
var streamId = req.params.stream
|
||||
var fullFilePath = Path.join(this.StreamsPath, streamId, req.params.file)
|
||||
var fullFilePath = Path.join(this.streamManager.StreamsPath, streamId, req.params.file)
|
||||
|
||||
// development test stream - ignore
|
||||
if (streamId === 'test') {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ const Logger = require('./Logger')
|
|||
const TAG = '[LogManager]'
|
||||
|
||||
class LogManager {
|
||||
constructor(MetadataPath, db) {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
|
||||
this.logDirPath = Path.join(this.MetadataPath, 'logs')
|
||||
this.logDirPath = Path.join(global.MetadataPath, 'logs')
|
||||
this.dailyLogDirPath = Path.join(this.logDirPath, 'daily')
|
||||
|
||||
this.currentDailyLog = null
|
||||
|
|
|
|||
|
|
@ -39,27 +39,35 @@ class Server {
|
|||
this.Uid = isNaN(UID) ? 0 : Number(UID)
|
||||
this.Gid = isNaN(GID) ? 0 : Number(GID)
|
||||
this.Host = '0.0.0.0'
|
||||
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
this.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
global.Uid = this.Uid
|
||||
global.Gid = this.Gid
|
||||
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
global.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
// Fix backslash if not on Windows
|
||||
if (process.platform !== 'win32') {
|
||||
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
|
||||
global.AudiobookPath = global.AudiobookPath.replace(/\\/g, '/')
|
||||
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
fs.ensureDirSync(CONFIG_PATH, 0o774)
|
||||
fs.ensureDirSync(METADATA_PATH, 0o774)
|
||||
fs.ensureDirSync(AUDIOBOOK_PATH, 0o774)
|
||||
fs.ensureDirSync(global.ConfigPath, 0o774)
|
||||
fs.ensureDirSync(global.MetadataPath, 0o774)
|
||||
fs.ensureDirSync(global.AudiobookPath, 0o774)
|
||||
|
||||
this.db = new Db(this.ConfigPath, this.AudiobookPath)
|
||||
this.db = new Db()
|
||||
this.auth = new Auth(this.db)
|
||||
this.backupManager = new BackupManager(this.MetadataPath, this.Uid, this.Gid, this.db)
|
||||
this.logManager = new LogManager(this.MetadataPath, this.db)
|
||||
this.cacheManager = new CacheManager(this.MetadataPath)
|
||||
this.watcher = new Watcher(this.AudiobookPath)
|
||||
this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath)
|
||||
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
|
||||
this.backupManager = new BackupManager(this.Uid, this.Gid, this.db)
|
||||
this.logManager = new LogManager(this.db)
|
||||
this.cacheManager = new CacheManager()
|
||||
this.watcher = new Watcher()
|
||||
this.coverController = new CoverController(this.db, this.cacheManager)
|
||||
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
||||
|
||||
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.rssFeeds = new RssFeeds(this.Port, this.db)
|
||||
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.Uid, this.Gid)
|
||||
this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.downloadManager = new DownloadManager(this.db, this.Uid, this.Gid)
|
||||
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
|
||||
|
||||
Logger.logManager = this.logManager
|
||||
|
|
@ -101,7 +109,7 @@ class Server {
|
|||
clientEmitter(userId, ev, data) {
|
||||
var clients = this.getClientsForUser(userId)
|
||||
if (!clients.length) {
|
||||
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
|
||||
return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`)
|
||||
}
|
||||
clients.forEach((client) => {
|
||||
if (client.socket) {
|
||||
|
|
@ -133,10 +141,18 @@ class Server {
|
|||
Logger.info(`[Server] Running scan for duplicate book IDs`)
|
||||
await this.scanner.fixDuplicateIds()
|
||||
}
|
||||
// If server upgrade and last version was 1.7.0 or earlier - add abmetadata files
|
||||
// if (this.db.checkPreviousVersionIsBefore('1.7.1')) {
|
||||
// TODO: wait until stable
|
||||
// }
|
||||
|
||||
this.watcher.initWatcher(this.libraries)
|
||||
this.watcher.on('files', this.filesChanged.bind(this))
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
Logger.info(`[Server] Watcher is disabled`)
|
||||
this.watcher.disabled = true
|
||||
} else {
|
||||
this.watcher.initWatcher(this.libraries)
|
||||
this.watcher.on('files', this.filesChanged.bind(this))
|
||||
}
|
||||
this.passportInit()
|
||||
}
|
||||
|
||||
|
|
@ -194,10 +210,10 @@ class Server {
|
|||
app.use(express.static(distPath))
|
||||
|
||||
// Old static path for covers
|
||||
app.use('/local', this.authMiddleware.bind(this), express.static(this.AudiobookPath))
|
||||
app.use('/local', this.authMiddleware.bind(this), express.static(global.AudiobookPath))
|
||||
|
||||
// Metadata folder static path
|
||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(this.MetadataPath))
|
||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||
|
||||
// Downloads folder static path
|
||||
app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
|
||||
|
|
@ -314,7 +330,6 @@ class Server {
|
|||
// Streaming
|
||||
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
||||
socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
|
||||
|
||||
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
||||
|
|
@ -399,7 +414,7 @@ class Server {
|
|||
|
||||
// Remove unused /metadata/books/{id} folders
|
||||
async purgeMetadata() {
|
||||
var booksMetadata = Path.join(this.MetadataPath, 'books')
|
||||
var booksMetadata = Path.join(global.MetadataPath, 'books')
|
||||
var booksMetadataExists = await fs.pathExists(booksMetadata)
|
||||
if (!booksMetadataExists) return
|
||||
var foldersInBooksMetadata = await fs.readdir(booksMetadata)
|
||||
|
|
@ -458,25 +473,27 @@ class Server {
|
|||
|
||||
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||
if (!library) {
|
||||
return res.status(500).error(`Library not found with id ${libraryId}`)
|
||||
return res.status(500).send(`Library not found with id ${libraryId}`)
|
||||
}
|
||||
var folder = library.folders.find(fold => fold.id === folderId)
|
||||
if (!folder) {
|
||||
return res.status(500).error(`Folder not found with id ${folderId} in library ${library.name}`)
|
||||
return res.status(500).send(`Folder not found with id ${folderId} in library ${library.name}`)
|
||||
}
|
||||
|
||||
if (!files.length || !title || !author) {
|
||||
return res.status(500).error(`Invalid post data`)
|
||||
if (!files.length || !title) {
|
||||
return res.status(500).send(`Invalid post data`)
|
||||
}
|
||||
|
||||
// For setting permissions recursively
|
||||
var firstDirPath = Path.join(folder.fullPath, author)
|
||||
|
||||
var outputDirectory = ''
|
||||
if (series && series.length && series !== 'null') {
|
||||
if (series && author) {
|
||||
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
||||
} else {
|
||||
} else if (author) {
|
||||
outputDirectory = Path.join(folder.fullPath, author, title)
|
||||
} else {
|
||||
outputDirectory = Path.join(folder.fullPath, title)
|
||||
}
|
||||
|
||||
var exists = await fs.pathExists(outputDirectory)
|
||||
|
|
@ -672,9 +689,9 @@ class Server {
|
|||
|
||||
const initialPayload = {
|
||||
serverSettings: this.serverSettings.toJSON(),
|
||||
audiobookPath: this.AudiobookPath,
|
||||
metadataPath: this.MetadataPath,
|
||||
configPath: this.ConfigPath,
|
||||
audiobookPath: global.AudiobookPath,
|
||||
metadataPath: global.MetadataPath,
|
||||
configPath: global.ConfigPath,
|
||||
user: client.user.toJSONForBrowser(),
|
||||
stream: client.stream || null,
|
||||
librariesScanning: this.scanner.librariesScanning,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@ const fs = require('fs-extra')
|
|||
const Path = require('path')
|
||||
|
||||
class StreamManager {
|
||||
constructor(db, MetadataPath, emitter, clientEmitter) {
|
||||
constructor(db, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.MetadataPath = MetadataPath
|
||||
this.streams = []
|
||||
this.StreamsPath = Path.join(this.MetadataPath, 'streams')
|
||||
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
||||
}
|
||||
|
||||
get audiobooks() {
|
||||
|
|
@ -68,12 +67,12 @@ class StreamManager {
|
|||
|
||||
async tempCheckStrayStreams() {
|
||||
try {
|
||||
var dirs = await fs.readdir(this.MetadataPath)
|
||||
var dirs = await fs.readdir(global.MetadataPath)
|
||||
if (!dirs || !dirs.length) return true
|
||||
|
||||
await Promise.all(dirs.map(async (dirname) => {
|
||||
if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') {
|
||||
var fullPath = Path.join(this.MetadataPath, dirname)
|
||||
var fullPath = Path.join(global.MetadataPath, dirname)
|
||||
Logger.warn(`Removing OLD Orphan Stream ${dirname}`)
|
||||
return fs.remove(fullPath)
|
||||
}
|
||||
|
|
@ -155,6 +154,30 @@ class StreamManager {
|
|||
this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
|
||||
}
|
||||
|
||||
async closeStreamApiRequest(userId, streamId) {
|
||||
Logger.info('[StreamManager] Close Stream Api Request', streamId)
|
||||
|
||||
var stream = this.streams.find(s => s.id === streamId)
|
||||
if (!stream) {
|
||||
Logger.warn('[StreamManager] Stream not found', streamId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!stream.client || !stream.client.user || stream.client.user.id !== userId) {
|
||||
Logger.warn(`[StreamManager] Stream close request from invalid user ${userId}`, stream.client)
|
||||
return
|
||||
}
|
||||
|
||||
stream.client.user.stream = null
|
||||
stream.client.stream = null
|
||||
this.db.updateUserStream(stream.client.user.id, null)
|
||||
|
||||
await stream.close()
|
||||
|
||||
this.streams = this.streams.filter(s => s.id !== streamId)
|
||||
Logger.info(`[StreamManager] Stream ${streamId} closed via API request by ${userId}`)
|
||||
}
|
||||
|
||||
streamSync(socket, syncData) {
|
||||
const client = socket.sheepClient
|
||||
if (!client || !client.stream) {
|
||||
|
|
@ -233,35 +256,5 @@ class StreamManager {
|
|||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
streamUpdate(socket, { currentTime, streamId }) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.stream) {
|
||||
Logger.error('No stream for client', (client && client.user) ? client.user.id : 'No Client')
|
||||
return
|
||||
}
|
||||
if (client.stream.id !== streamId) {
|
||||
Logger.error('Stream id mismatch on stream update', streamId, client.stream.id)
|
||||
return
|
||||
}
|
||||
client.stream.updateClientCurrentTime(currentTime)
|
||||
if (!client.user) {
|
||||
Logger.error('No User for client', client)
|
||||
return
|
||||
}
|
||||
if (!client.user.updateAudiobookProgressFromStream) {
|
||||
Logger.error('Invalid User for client', client)
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
|
||||
this.db.updateEntity('user', client.user)
|
||||
|
||||
if (userAudiobook) {
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook.toJSON()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = StreamManager
|
||||
|
|
@ -13,6 +13,8 @@ class FolderWatcher extends EventEmitter {
|
|||
this.pendingFileUpdates = []
|
||||
this.pendingDelay = 4000
|
||||
this.pendingTimeout = null
|
||||
|
||||
this.disabled = false
|
||||
}
|
||||
|
||||
get pendingFilePaths() {
|
||||
|
|
@ -66,15 +68,19 @@ class FolderWatcher extends EventEmitter {
|
|||
|
||||
initWatcher(libraries) {
|
||||
libraries.forEach((lib) => {
|
||||
this.buildLibraryWatcher(lib)
|
||||
if (!lib.disableWatcher) {
|
||||
this.buildLibraryWatcher(lib)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addLibrary(library) {
|
||||
if (this.disabled || library.disableWatcher) return
|
||||
this.buildLibraryWatcher(library)
|
||||
}
|
||||
|
||||
updateLibrary(library) {
|
||||
if (this.disabled || library.disableWatcher) return
|
||||
var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id)
|
||||
if (libwatcher) {
|
||||
libwatcher.name = library.name
|
||||
|
|
@ -90,6 +96,7 @@ class FolderWatcher extends EventEmitter {
|
|||
}
|
||||
|
||||
removeLibrary(library) {
|
||||
if (this.disabled) return
|
||||
var libwatcher = this.libraryWatchers.find(lib => lib.id === library.id)
|
||||
if (libwatcher) {
|
||||
Logger.info(`[Watcher] Removed watcher for "${library.name}"`)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ class BookController {
|
|||
}
|
||||
|
||||
findOne(req, res) {
|
||||
if (!req.user) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
|
|
@ -48,8 +45,8 @@ class BookController {
|
|||
var hasUpdates = audiobook.update(req.body)
|
||||
if (hasUpdates) {
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
}
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
res.json(audiobook.toJSON())
|
||||
}
|
||||
|
||||
|
|
@ -259,5 +256,24 @@ class BookController {
|
|||
}
|
||||
return this.cacheManager.handleCoverCache(res, audiobook, options)
|
||||
}
|
||||
|
||||
// POST api/books/:id/match
|
||||
async match(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to match without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this audiobooks library
|
||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var options = req.body || {}
|
||||
var matchResult = await this.scanner.quickMatchBook(audiobook, options)
|
||||
res.json(matchResult)
|
||||
}
|
||||
}
|
||||
module.exports = new BookController()
|
||||
|
|
@ -144,10 +144,19 @@ class LibraryController {
|
|||
}
|
||||
|
||||
if (payload.sortBy) {
|
||||
var sortKey = payload.sortBy
|
||||
|
||||
// Handle server setting sortingIgnorePrefix
|
||||
if ((sortKey === 'book.series' || sortKey === 'book.title') && this.db.serverSettings.sortingIgnorePrefix) {
|
||||
// Book.js has seriesIgnorePrefix and titleIgnorePrefix getters
|
||||
sortKey += 'IgnorePrefix'
|
||||
}
|
||||
|
||||
var direction = payload.sortDesc ? 'desc' : 'asc'
|
||||
audiobooks = naturalSort(audiobooks)[direction]((ab) => {
|
||||
|
||||
// Supports dot notation strings i.e. "book.title"
|
||||
return payload.sortBy.split('.').reduce((a, b) => a[b], ab)
|
||||
return sortKey.split('.').reduce((a, b) => a[b], ab)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -202,7 +211,14 @@ class LibraryController {
|
|||
}
|
||||
|
||||
var series = libraryHelpers.getSeriesFromBooks(audiobooks, payload.minified)
|
||||
series = sort(series).asc(s => s.name)
|
||||
|
||||
var sortingIgnorePrefix = this.db.serverSettings.sortingIgnorePrefix
|
||||
series = sort(series).asc(s => {
|
||||
if (sortingIgnorePrefix && s.name.toLowerCase().startsWith('the ')) {
|
||||
return s.name.substr(4)
|
||||
}
|
||||
return s.name
|
||||
})
|
||||
payload.total = series.length
|
||||
|
||||
if (payload.limit) {
|
||||
|
|
@ -318,7 +334,7 @@ class LibraryController {
|
|||
async reorder(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
|
||||
return res.sendStatus(401)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var orderdata = req.body
|
||||
|
|
@ -454,6 +470,15 @@ class LibraryController {
|
|||
res.json(Object.values(authors))
|
||||
}
|
||||
|
||||
async matchBooks(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
Logger.error(`[LibraryController] Non-root user attempted to match library books`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
this.scanner.matchLibraryBooks(req.library)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var librariesAccessible = req.user.librariesAccessible || []
|
||||
if (librariesAccessible && librariesAccessible.length && !librariesAccessible.includes(req.params.id)) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ class AudioFile {
|
|||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
this.addedAt = null
|
||||
|
||||
this.trackNumFromMeta = null
|
||||
|
|
@ -51,6 +54,9 @@ class AudioFile {
|
|||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
trackNumFromMeta: this.trackNumFromMeta,
|
||||
discNumFromMeta: this.discNumFromMeta,
|
||||
|
|
@ -82,6 +88,9 @@ class AudioFile {
|
|||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = data.addedAt
|
||||
this.manuallyVerified = !!data.manuallyVerified
|
||||
this.invalid = !!data.invalid
|
||||
|
|
@ -124,6 +133,9 @@ class AudioFile {
|
|||
this.ext = fileData.ext
|
||||
this.path = fileData.path
|
||||
this.fullPath = fileData.fullPath
|
||||
this.mtimeMs = fileData.mtimeMs || 0
|
||||
this.ctimeMs = fileData.ctimeMs || 0
|
||||
this.birthtimeMs = fileData.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
|
||||
this.trackNumFromMeta = fileData.trackNumFromMeta
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const { bytesPretty, readTextFile } = require('../utils/fileUtils')
|
||||
const { comparePaths, getIno, getId, elapsedPretty } = require('../utils/index')
|
||||
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
|
||||
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
|
||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||
const nfoGenerator = require('../utils/nfoGenerator')
|
||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
||||
const Logger = require('../Logger')
|
||||
const Book = require('./Book')
|
||||
const AudioTrack = require('./AudioTrack')
|
||||
|
|
@ -21,6 +22,9 @@ class Audiobook {
|
|||
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
this.addedAt = null
|
||||
this.lastUpdate = null
|
||||
this.lastScan = null
|
||||
|
|
@ -44,6 +48,9 @@ class Audiobook {
|
|||
if (audiobook) {
|
||||
this.construct(audiobook)
|
||||
}
|
||||
|
||||
// Temp flags
|
||||
this.isSavingMetadata = false
|
||||
}
|
||||
|
||||
construct(audiobook) {
|
||||
|
|
@ -53,6 +60,9 @@ class Audiobook {
|
|||
this.folderId = audiobook.folderId || 'audiobooks'
|
||||
this.path = audiobook.path
|
||||
this.fullPath = audiobook.fullPath
|
||||
this.mtimeMs = audiobook.mtimeMs || 0
|
||||
this.ctimeMs = audiobook.ctimeMs || 0
|
||||
this.birthtimeMs = audiobook.birthtimeMs || 0
|
||||
this.addedAt = audiobook.addedAt
|
||||
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
||||
this.lastScan = audiobook.lastScan || null
|
||||
|
|
@ -173,11 +183,11 @@ class Audiobook {
|
|||
ino: this.ino,
|
||||
libraryId: this.libraryId,
|
||||
folderId: this.folderId,
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
cover: this.cover,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
lastScan: this.lastScan,
|
||||
|
|
@ -204,6 +214,9 @@ class Audiobook {
|
|||
tags: this.tags,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
duration: this.duration,
|
||||
|
|
@ -227,6 +240,9 @@ class Audiobook {
|
|||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
duration: this.duration,
|
||||
|
|
@ -334,6 +350,9 @@ class Audiobook {
|
|||
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
this.lastUpdate = this.addedAt
|
||||
|
||||
|
|
@ -425,13 +444,8 @@ class Audiobook {
|
|||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (payload.book) {
|
||||
if (!this.book) {
|
||||
this.setBook(payload.book)
|
||||
hasUpdates = true
|
||||
} else if (this.book.update(payload.book)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
if (payload.book && this.book.update(payload.book)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
|
|
@ -526,7 +540,7 @@ class Audiobook {
|
|||
}
|
||||
|
||||
// On scan check other files found with other files saved
|
||||
async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
|
||||
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
|
||||
var hasUpdates = false
|
||||
|
||||
var currOtherFileNum = this.otherFiles.length
|
||||
|
|
@ -535,6 +549,8 @@ class Audiobook {
|
|||
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
|
||||
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
|
||||
|
||||
var existingAbMetadata = this.otherFiles.find(file => file.filename === 'metadata.abs')
|
||||
|
||||
// Filter out other files no longer in directory
|
||||
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
||||
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
||||
|
|
@ -543,9 +559,9 @@ class Audiobook {
|
|||
hasUpdates = true
|
||||
}
|
||||
|
||||
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
|
||||
// If desc.txt is new then read it and update description (will overwrite)
|
||||
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
|
||||
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
|
||||
if (descriptionTxt && !alreadyHasDescTxt) {
|
||||
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
||||
if (newDescription) {
|
||||
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
|
||||
|
|
@ -553,9 +569,9 @@ class Audiobook {
|
|||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
|
||||
// If reader.txt is new then read it and update narrator (will overwrite)
|
||||
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
|
||||
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
||||
if (readerTxt && !alreadyHasReaderTxt) {
|
||||
var newReader = await readTextFile(readerTxt.fullPath)
|
||||
if (newReader) {
|
||||
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
|
||||
|
|
@ -564,7 +580,28 @@ class Audiobook {
|
|||
}
|
||||
}
|
||||
|
||||
// If OPF file and was not already there
|
||||
|
||||
// If metadata.abs is new OR modified then read it and set all defined keys (will overwrite)
|
||||
var metadataAbs = newOtherFiles.find(file => file.filename === 'metadata.abs')
|
||||
var shouldUpdateAbs = !!metadataAbs && (metadataAbs.modified || !existingAbMetadata)
|
||||
if (metadataAbs && metadataAbs.modified) {
|
||||
Logger.debug(`[Audiobook] metadata.abs file was modified for "${this.title}"`)
|
||||
}
|
||||
|
||||
if (shouldUpdateAbs) {
|
||||
var abmetadataText = await readTextFile(metadataAbs.fullPath)
|
||||
if (abmetadataText) {
|
||||
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
|
||||
if (metadataUpdateObject && metadataUpdateObject.book) {
|
||||
if (this.update(metadataUpdateObject)) {
|
||||
Logger.debug(`[Audiobook] Some details were updated from metadata.abs for "${this.title}"`, metadataUpdateObject)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If OPF file and was not already there OR prefer opf metadata
|
||||
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
|
||||
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
|
||||
var xmlText = await readTextFile(metadataOpf.fullPath)
|
||||
|
|
@ -643,7 +680,7 @@ class Audiobook {
|
|||
if (bookCoverPath && bookCoverPath.startsWith('/metadata')) {
|
||||
// Fixing old cover paths
|
||||
if (!this.book.coverFullPath) {
|
||||
this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
|
||||
this.book.coverFullPath = Path.join(global.MetadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
|
||||
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
||||
hasUpdates = true
|
||||
}
|
||||
|
|
@ -800,9 +837,10 @@ class Audiobook {
|
|||
return false
|
||||
}
|
||||
|
||||
// Look for desc.txt and reader.txt and update details if found
|
||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
|
||||
var bookUpdatePayload = {}
|
||||
|
||||
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
|
||||
if (descriptionText) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
|
||||
|
|
@ -814,6 +852,22 @@ class Audiobook {
|
|||
bookUpdatePayload.narrator = readerText
|
||||
}
|
||||
|
||||
// abmetadata will always overwrite
|
||||
var abmetadataText = await this.fetchTextFromTextFile('metadata.abs')
|
||||
if (abmetadataText) {
|
||||
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
|
||||
if (metadataUpdateObject && metadataUpdateObject.book) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" found metadata.abs file`)
|
||||
for (const key in metadataUpdateObject.book) {
|
||||
var value = metadataUpdateObject.book[key]
|
||||
if (key && value !== undefined) {
|
||||
bookUpdatePayload[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opf only overwrites if detail is empty
|
||||
var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
|
||||
if (metadataOpf) {
|
||||
var xmlText = await readTextFile(metadataOpf.fullPath)
|
||||
|
|
@ -873,12 +927,6 @@ class Audiobook {
|
|||
}
|
||||
}
|
||||
|
||||
if (existingFile.filename !== fileFound.filename) {
|
||||
existingFile.filename = fileFound.filename
|
||||
existingFile.ext = fileFound.ext
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
if (existingFile.path !== fileFound.path) {
|
||||
existingFile.path = fileFound.path
|
||||
existingFile.fullPath = fileFound.fullPath
|
||||
|
|
@ -888,6 +936,20 @@ class Audiobook {
|
|||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
|
||||
keysToCheck.forEach((key) => {
|
||||
if (existingFile[key] !== fileFound[key]) {
|
||||
|
||||
// Add modified flag on file data object if exists and was changed
|
||||
if (key === 'mtimeMs' && existingFile[key]) {
|
||||
fileFound.modified = true
|
||||
}
|
||||
|
||||
existingFile[key] = fileFound[key]
|
||||
hasUpdated = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
|
||||
existingFile.filetype = fileFound.filetype
|
||||
hasUpdated = true
|
||||
|
|
@ -927,6 +989,14 @@ class Audiobook {
|
|||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
|
||||
keysToCheck.forEach((key) => {
|
||||
if (dataFound[key] != this[key]) {
|
||||
this[key] = dataFound[key] || 0
|
||||
hasUpdated = true
|
||||
}
|
||||
})
|
||||
|
||||
var newAudioFileData = []
|
||||
var newOtherFileData = []
|
||||
var existingAudioFileData = []
|
||||
|
|
@ -1017,14 +1087,14 @@ class Audiobook {
|
|||
}
|
||||
|
||||
// Temp fix for cover is set but coverFullPath is not set
|
||||
fixFullCoverPath(metadataPath) {
|
||||
fixFullCoverPath() {
|
||||
if (!this.book.cover) return
|
||||
var bookCoverPath = this.book.cover.replace(/\\/g, '/')
|
||||
var newFullCoverPath = null
|
||||
if (bookCoverPath.startsWith('/s/book/')) {
|
||||
newFullCoverPath = Path.join(this.fullPath, bookCoverPath.substr(`/s/book/${this.id}`.length)).replace(/\/\//g, '/')
|
||||
} else if (bookCoverPath.startsWith('/metadata/')) {
|
||||
newFullCoverPath = Path.join(metadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
|
||||
newFullCoverPath = Path.join(global.MetadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
|
||||
}
|
||||
if (newFullCoverPath) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" fixing full cover path "${this.book.cover}" => "${newFullCoverPath}"`)
|
||||
|
|
@ -1033,5 +1103,26 @@ class Audiobook {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async saveAbMetadata() {
|
||||
if (this.isSavingMetadata) return
|
||||
this.isSavingMetadata = true
|
||||
|
||||
var metadataPath = Path.join(global.MetadataPath, 'books', this.id)
|
||||
if (global.ServerSettings.storeMetadataWithBook) {
|
||||
metadataPath = this.fullPath
|
||||
} else {
|
||||
// Make sure metadata book dir exists
|
||||
await fs.ensureDir(metadataPath)
|
||||
}
|
||||
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
||||
|
||||
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
||||
this.isSavingMetadata = false
|
||||
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
|
||||
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
|
||||
return success
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = Audiobook
|
||||
|
|
@ -6,6 +6,11 @@ class AudiobookFile {
|
|||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.size = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
|
||||
this.addedAt = null
|
||||
|
||||
if (data) {
|
||||
|
|
@ -25,6 +30,10 @@ class AudiobookFile {
|
|||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
size: this.size,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +45,10 @@ class AudiobookFile {
|
|||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.size = data.size || 0
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = data.addedAt
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +59,10 @@ class AudiobookFile {
|
|||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.size = data.size || 0
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,20 @@ class Book {
|
|||
get _asin() { return this.asin || '' }
|
||||
get genresCommaSeparated() { return this._genres.join(', ') }
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
if (this._title.toLowerCase().startsWith('the ')) {
|
||||
return this._title.substr(4) + ', The'
|
||||
}
|
||||
return this._title
|
||||
}
|
||||
|
||||
get seriesIgnorePrefix() {
|
||||
if (this._series.toLowerCase().startsWith('the ')) {
|
||||
return this._series.substr(4) + ', The'
|
||||
}
|
||||
return this._series
|
||||
}
|
||||
|
||||
get shouldSearchForCover() {
|
||||
if (this.cover) return false
|
||||
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ class Library {
|
|||
this.folders = []
|
||||
this.displayOrder = 1
|
||||
this.icon = 'database'
|
||||
this.provider = 'google'
|
||||
this.disableWatcher = false
|
||||
|
||||
this.lastScan = 0
|
||||
|
||||
|
|
@ -29,6 +31,8 @@ class Library {
|
|||
this.folders = (library.folders || []).map(f => new Folder(f))
|
||||
this.displayOrder = library.displayOrder || 1
|
||||
this.icon = library.icon || 'database'
|
||||
this.provider = library.provider || 'google'
|
||||
this.disableWatcher = !!library.disableWatcher
|
||||
|
||||
this.createdAt = library.createdAt
|
||||
this.lastUpdate = library.lastUpdate
|
||||
|
|
@ -41,6 +45,8 @@ class Library {
|
|||
folders: (this.folders || []).map(f => f.toJSON()),
|
||||
displayOrder: this.displayOrder,
|
||||
icon: this.icon,
|
||||
provider: this.provider,
|
||||
disableWatcher: this.disableWatcher,
|
||||
createdAt: this.createdAt,
|
||||
lastUpdate: this.lastUpdate
|
||||
}
|
||||
|
|
@ -65,6 +71,7 @@ class Library {
|
|||
}
|
||||
this.displayOrder = data.displayOrder || 1
|
||||
this.icon = data.icon || 'database'
|
||||
this.disableWatcher = !!data.disableWatcher
|
||||
this.createdAt = Date.now()
|
||||
this.lastUpdate = Date.now()
|
||||
}
|
||||
|
|
@ -75,6 +82,14 @@ class Library {
|
|||
this.name = payload.name
|
||||
hasUpdates = true
|
||||
}
|
||||
if (payload.provider && payload.provider !== this.provider) {
|
||||
this.provider = payload.provider
|
||||
hasUpdates = true
|
||||
}
|
||||
if (payload.disableWatcher !== this.disableWatcher) {
|
||||
this.disableWatcher = !!payload.disableWatcher
|
||||
hasUpdates = true
|
||||
}
|
||||
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
|
||||
this.displayOrder = Number(payload.displayOrder)
|
||||
hasUpdates = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { CoverDestination, BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
|
||||
const { BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class ServerSettings {
|
||||
|
|
@ -15,10 +15,11 @@ class ServerSettings {
|
|||
this.scannerCoverProvider = 'google'
|
||||
this.scannerPreferAudioMetadata = false
|
||||
this.scannerPreferOpfMetadata = false
|
||||
this.scannerDisableWatcher = false
|
||||
|
||||
// Metadata
|
||||
this.coverDestination = CoverDestination.METADATA
|
||||
this.saveMetadataFile = false
|
||||
this.storeCoverWithBook = false
|
||||
this.storeMetadataWithBook = false
|
||||
|
||||
// Security/Rate limits
|
||||
this.rateLimitLoginRequests = 10
|
||||
|
|
@ -38,6 +39,8 @@ class ServerSettings {
|
|||
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
||||
this.bookshelfView = BookshelfView.STANDARD
|
||||
|
||||
this.sortingIgnorePrefix = false
|
||||
this.chromecastEnabled = false
|
||||
this.logLevel = Logger.logLevel
|
||||
this.version = null
|
||||
|
||||
|
|
@ -54,9 +57,14 @@ class ServerSettings {
|
|||
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
||||
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
|
||||
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||
|
||||
this.storeCoverWithBook = settings.storeCoverWithBook
|
||||
if (this.storeCoverWithBook == undefined) { // storeCoverWithBook added in 1.7.1 to replace coverDestination
|
||||
this.storeCoverWithBook = !!settings.coverDestination
|
||||
}
|
||||
this.storeMetadataWithBook = !!settings.storeCoverWithBook
|
||||
|
||||
this.coverDestination = settings.coverDestination || CoverDestination.METADATA
|
||||
this.saveMetadataFile = !!settings.saveMetadataFile
|
||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||
|
||||
|
|
@ -70,6 +78,8 @@ class ServerSettings {
|
|||
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
|
||||
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
|
||||
|
||||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||
this.logLevel = settings.logLevel || Logger.logLevel
|
||||
this.version = settings.version || null
|
||||
|
||||
|
|
@ -88,8 +98,9 @@ class ServerSettings {
|
|||
scannerParseSubtitle: this.scannerParseSubtitle,
|
||||
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
|
||||
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
||||
coverDestination: this.coverDestination,
|
||||
saveMetadataFile: !!this.saveMetadataFile,
|
||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||
storeCoverWithBook: this.storeCoverWithBook,
|
||||
storeMetadataWithBook: this.storeMetadataWithBook,
|
||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||
backupSchedule: this.backupSchedule,
|
||||
|
|
@ -99,6 +110,8 @@ class ServerSettings {
|
|||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||
coverAspectRatio: this.coverAspectRatio,
|
||||
bookshelfView: this.bookshelfView,
|
||||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
chromecastEnabled: this.chromecastEnabled,
|
||||
logLevel: this.logLevel,
|
||||
version: this.version
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,11 +175,6 @@ class Stream extends EventEmitter {
|
|||
return false
|
||||
}
|
||||
|
||||
updateClientCurrentTime(currentTime) {
|
||||
Logger.debug('[Stream] Updated client current time', secondsToTimestamp(currentTime))
|
||||
this.clientCurrentTime = currentTime
|
||||
}
|
||||
|
||||
syncStream({ timeListened, currentTime }) {
|
||||
var syncLog = ''
|
||||
// Set user current time
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ class Audible {
|
|||
}
|
||||
|
||||
getBestImageLink(images) {
|
||||
var keys = Object.keys(images);
|
||||
return images[keys[keys.length - 1]];
|
||||
if (!images) return null
|
||||
var keys = Object.keys(images)
|
||||
if (!keys.length) return null
|
||||
return images[keys[keys.length - 1]]
|
||||
}
|
||||
|
||||
getPrimarySeries(series, publication_name) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
const AuthorFinder = require('../AuthorFinder')
|
||||
|
||||
class AuthorScanner {
|
||||
constructor(db, MetadataPath) {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
this.authorFinder = new AuthorFinder(MetadataPath)
|
||||
this.authorFinder = new AuthorFinder()
|
||||
}
|
||||
|
||||
getUniqueAuthors() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const { getId, secondsToTimestamp } = require('../utils/index')
|
|||
class LibraryScan {
|
||||
constructor() {
|
||||
this.id = null
|
||||
this.type = null
|
||||
this.libraryId = null
|
||||
this.libraryName = null
|
||||
this.folders = null
|
||||
|
|
@ -46,6 +47,7 @@ class LibraryScan {
|
|||
get getScanEmitData() {
|
||||
return {
|
||||
id: this.libraryId,
|
||||
type: this.type,
|
||||
name: this.libraryName,
|
||||
results: {
|
||||
added: this.resultsAdded,
|
||||
|
|
@ -64,10 +66,11 @@ class LibraryScan {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
libraryId: this.libraryId,
|
||||
libraryName: this.libraryName,
|
||||
folders: this.folders.map(f => f.toJSON()),
|
||||
scanOptions: this.scanOptions.toJSON(),
|
||||
scanOptions: this.scanOptions ? this.scanOptions.toJSON() : null,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt,
|
||||
elapsed: this.elapsed,
|
||||
|
|
@ -77,8 +80,9 @@ class LibraryScan {
|
|||
}
|
||||
}
|
||||
|
||||
setData(library, scanOptions) {
|
||||
setData(library, scanOptions, type = 'scan') {
|
||||
this.id = getId('lscan')
|
||||
this.type = type
|
||||
this.libraryId = library.id
|
||||
this.libraryName = library.name
|
||||
this.folders = library.folders.map(folder => new Folder(folder.toJSON()))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
const { CoverDestination } = require('../utils/constants')
|
||||
|
||||
class ScanOptions {
|
||||
constructor(options) {
|
||||
this.forceRescan = false
|
||||
|
|
@ -7,7 +5,7 @@ class ScanOptions {
|
|||
// Server settings
|
||||
this.parseSubtitles = false
|
||||
this.findCovers = false
|
||||
this.coverDestination = CoverDestination.METADATA
|
||||
this.storeCoverWithBook = false
|
||||
this.preferAudioMetadata = false
|
||||
this.preferOpfMetadata = false
|
||||
|
||||
|
|
@ -32,7 +30,7 @@ class ScanOptions {
|
|||
metadataPrecedence: this.metadataPrecedence,
|
||||
parseSubtitles: this.parseSubtitles,
|
||||
findCovers: this.findCovers,
|
||||
coverDestination: this.coverDestination,
|
||||
storeCoverWithBook: this.storeCoverWithBook,
|
||||
preferAudioMetadata: this.preferAudioMetadata,
|
||||
preferOpfMetadata: this.preferOpfMetadata
|
||||
}
|
||||
|
|
@ -43,7 +41,7 @@ class ScanOptions {
|
|||
|
||||
this.parseSubtitles = !!serverSettings.scannerParseSubtitle
|
||||
this.findCovers = !!serverSettings.scannerFindCovers
|
||||
this.coverDestination = serverSettings.coverDestination
|
||||
this.storeCoverWithBook = serverSettings.storeCoverWithBook
|
||||
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
||||
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ const Path = require('path')
|
|||
const Logger = require('../Logger')
|
||||
const { version } = require('../../package.json')
|
||||
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
|
||||
const { comparePaths, getIno, getId, msToTimestamp } = require('../utils/index')
|
||||
const { ScanResult, CoverDestination, LogLevel } = require('../utils/constants')
|
||||
const { comparePaths, getId } = require('../utils/index')
|
||||
const { ScanResult, LogLevel } = require('../utils/constants')
|
||||
|
||||
const AudioFileScanner = require('./AudioFileScanner')
|
||||
const BookFinder = require('../BookFinder')
|
||||
|
|
@ -15,12 +15,9 @@ const LibraryScan = require('./LibraryScan')
|
|||
const ScanOptions = require('./ScanOptions')
|
||||
|
||||
class Scanner {
|
||||
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) {
|
||||
this.AudiobookPath = AUDIOBOOK_PATH
|
||||
this.MetadataPath = METADATA_PATH
|
||||
this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
|
||||
var LogDirPath = Path.join(this.MetadataPath, 'logs')
|
||||
this.ScanLogPath = Path.join(LogDirPath, 'scans')
|
||||
constructor(db, coverController, emitter) {
|
||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||
|
||||
this.db = db
|
||||
this.coverController = coverController
|
||||
|
|
@ -33,7 +30,7 @@ class Scanner {
|
|||
}
|
||||
|
||||
getCoverDirectory(audiobook) {
|
||||
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
|
||||
if (this.db.serverSettings.storeCoverWithBook) {
|
||||
return {
|
||||
fullPath: audiobook.fullPath,
|
||||
relPath: '/s/book/' + audiobook.id
|
||||
|
|
@ -88,8 +85,8 @@ class Scanner {
|
|||
|
||||
// Sync other files first so that local images are used as cover art
|
||||
// TODO: Cleanup other file sync
|
||||
var allOtherFiles = checkRes.newOtherFileData.concat(audiobook._otherFiles)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, this.db.serverSettings.scannerPreferOpfMetadata)) {
|
||||
var allOtherFiles = checkRes.newOtherFileData.concat(checkRes.existingOtherFileData)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.db.serverSettings.scannerPreferOpfMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +117,7 @@ class Scanner {
|
|||
|
||||
if (hasUpdated) {
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
await this.db.updateEntity('audiobook', audiobook)
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
return ScanResult.UPDATED
|
||||
}
|
||||
return ScanResult.UPTODATE
|
||||
|
|
@ -208,6 +205,7 @@ class Scanner {
|
|||
// Check for existing & removed audiobooks
|
||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||
var audiobook = audiobooksInLibrary[i]
|
||||
// Find audiobook folder with matching inode or matching path
|
||||
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
|
||||
if (!dataFound) {
|
||||
libraryScan.addLog(LogLevel.WARN, `Audiobook "${audiobook.title}" is missing`)
|
||||
|
|
@ -317,7 +315,7 @@ class Scanner {
|
|||
}))
|
||||
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
|
||||
libraryScan.resultsAdded += newAudiobooks.length
|
||||
await this.db.insertEntities('audiobook', newAudiobooks)
|
||||
await this.db.insertAudiobooks(newAudiobooks)
|
||||
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +328,7 @@ class Scanner {
|
|||
if (newOtherFileData.length || libraryScan.scanOptions.forceRescan) {
|
||||
// TODO: Cleanup other file sync
|
||||
var allOtherFiles = newOtherFileData.concat(existingOtherFileData)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, libraryScan.preferOpfMetadata)) {
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, libraryScan.preferOpfMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
|
@ -525,7 +523,7 @@ class Scanner {
|
|||
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
|
||||
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
|
||||
if (newAudiobook) {
|
||||
await this.db.insertEntity('audiobook', newAudiobook)
|
||||
await this.db.insertAudiobook(newAudiobook)
|
||||
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
|
||||
}
|
||||
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
|
||||
|
|
@ -615,7 +613,7 @@ class Scanner {
|
|||
}
|
||||
Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id)
|
||||
await this.db.removeEntity('audiobook', ab.id)
|
||||
await this.db.insertEntity('audiobook', abCopy)
|
||||
await this.db.insertAudiobook(abCopy)
|
||||
audiobooksUpdated++
|
||||
} else {
|
||||
ids[ab.id] = true
|
||||
|
|
@ -625,5 +623,101 @@ class Scanner {
|
|||
Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`)
|
||||
}
|
||||
}
|
||||
|
||||
async quickMatchBook(audiobook, options = {}) {
|
||||
var provider = options.provider || 'google'
|
||||
var searchTitle = options.title || audiobook.book._title
|
||||
var searchAuthor = options.author || audiobook.book._author
|
||||
|
||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
|
||||
if (!results.length) {
|
||||
return {
|
||||
warning: `No ${provider} match found`
|
||||
}
|
||||
}
|
||||
var matchData = results[0]
|
||||
|
||||
// Update cover if not set OR overrideCover flag
|
||||
var hasUpdated = false
|
||||
if (matchData.cover && (!audiobook.book.cover || options.overrideCover)) {
|
||||
Logger.debug(`[BookController] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverController.downloadCoverFromUrl(audiobook, matchData.cover)
|
||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||
Logger.warn(`[BookController] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
} else {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update book details if not set OR overrideDetails flag
|
||||
const detailKeysToUpdate = ['title', 'subtitle', 'author', 'narrator', 'publisher', 'publishYear', 'series', 'volumeNumber', 'asin', 'isbn']
|
||||
const updatePayload = {}
|
||||
for (const key in matchData) {
|
||||
if (matchData[key] && detailKeysToUpdate.includes(key) && (!audiobook.book[key] || options.overrideDetails)) {
|
||||
updatePayload[key] = matchData[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
Logger.debug('[BookController] Updating details', updatePayload)
|
||||
if (audiobook.update({ book: updatePayload })) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
}
|
||||
|
||||
return {
|
||||
updated: hasUpdated,
|
||||
audiobook: audiobook.toJSONExpanded()
|
||||
}
|
||||
}
|
||||
|
||||
async matchLibraryBooks(library) {
|
||||
if (this.isLibraryScanning(library.id)) {
|
||||
Logger.error(`[Scanner] Already scanning ${library.id}`)
|
||||
return
|
||||
}
|
||||
|
||||
const provider = library.provider || 'google'
|
||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
||||
if (!audiobooksInLibrary.length) {
|
||||
return
|
||||
}
|
||||
|
||||
var libraryScan = new LibraryScan()
|
||||
libraryScan.setData(library, null, 'match')
|
||||
this.librariesScanning.push(libraryScan.getScanEmitData)
|
||||
this.emitter('scan_start', libraryScan.getScanEmitData)
|
||||
|
||||
Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
|
||||
|
||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||
var audiobook = audiobooksInLibrary[i]
|
||||
Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
|
||||
var result = await this.quickMatchBook(audiobook, { provider })
|
||||
if (result.warning) {
|
||||
Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
|
||||
} else if (result.updated) {
|
||||
libraryScan.resultsUpdated++
|
||||
}
|
||||
|
||||
if (this.cancelLibraryScan[libraryScan.libraryId]) {
|
||||
Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
|
||||
delete this.cancelLibraryScan[libraryScan.libraryId]
|
||||
var scanData = libraryScan.getScanEmitData
|
||||
scanData.results = false
|
||||
this.emitter('scan_complete', scanData)
|
||||
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
||||
this.emitter('scan_complete', libraryScan.getScanEmitData)
|
||||
}
|
||||
}
|
||||
module.exports = Scanner
|
||||
102
server/utils/abmetadataGenerator.js
Normal file
102
server/utils/abmetadataGenerator.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
const fs = require('fs-extra')
|
||||
const filePerms = require('./filePerms')
|
||||
const package = require('../../package.json')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const bookKeyMap = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
author: 'authorFL',
|
||||
narrator: 'narratorFL',
|
||||
series: 'series',
|
||||
volumeNumber: 'volumeNumber',
|
||||
publishYear: 'publishYear',
|
||||
publisher: 'publisher',
|
||||
description: 'description',
|
||||
isbn: 'isbn',
|
||||
asin: 'asin',
|
||||
language: 'language',
|
||||
genres: 'genresCommaSeparated'
|
||||
}
|
||||
|
||||
function generate(audiobook, outputPath) {
|
||||
var fileString = ';ABMETADATA1\n'
|
||||
fileString += `#audiobookshelf v${package.version}\n\n`
|
||||
|
||||
for (const key in bookKeyMap) {
|
||||
const value = audiobook.book[bookKeyMap[key]] || ''
|
||||
fileString += `${key}=${value}\n`
|
||||
}
|
||||
|
||||
if (audiobook.chapters.length) {
|
||||
fileString += '\n'
|
||||
audiobook.chapters.forEach((chapter) => {
|
||||
fileString += `[CHAPTER]\n`
|
||||
fileString += `start=${chapter.start}\n`
|
||||
fileString += `end=${chapter.end}\n`
|
||||
fileString += `title=${chapter.title}\n`
|
||||
})
|
||||
}
|
||||
|
||||
return fs.writeFile(outputPath, fileString).then(() => {
|
||||
return filePerms(outputPath, 0o774, global.Uid, global.Gid, true).then((data) => true)
|
||||
}).catch((error) => {
|
||||
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
module.exports.generate = generate
|
||||
|
||||
function parseAbMetadataText(text) {
|
||||
if (!text) return null
|
||||
var lines = text.split(/\r?\n/)
|
||||
|
||||
// Check first line and get abmetadata version number
|
||||
var firstLine = lines.shift().toLowerCase()
|
||||
if (!firstLine.startsWith(';abmetadata')) {
|
||||
Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`)
|
||||
return null
|
||||
}
|
||||
var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim())
|
||||
if (isNaN(abmetadataVersion)) {
|
||||
Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - using 1`)
|
||||
abmetadataVersion = 1
|
||||
}
|
||||
|
||||
// Remove comments and empty lines
|
||||
const ignoreFirstChars = [' ', '#', ';'] // Ignore any line starting with the following
|
||||
lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0]))
|
||||
|
||||
// Get lines that map to book details (all lines before the first chapter section)
|
||||
var firstSectionLine = lines.findIndex(l => l.startsWith('['))
|
||||
var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines
|
||||
|
||||
// Put valid book detail values into map
|
||||
const bookDetails = {}
|
||||
for (let i = 0; i < detailLines.length; i++) {
|
||||
var line = detailLines[i]
|
||||
var keyValue = line.split('=')
|
||||
if (keyValue.length < 2) {
|
||||
Logger.warn('abmetadata invalid line has no =', line)
|
||||
} else if (!bookKeyMap[keyValue[0].trim()]) {
|
||||
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid book detail key`)
|
||||
} else {
|
||||
var key = keyValue[0].trim()
|
||||
bookDetails[key] = keyValue[1].trim()
|
||||
|
||||
// Genres convert to array of strings
|
||||
if (key === 'genres') {
|
||||
bookDetails[key] = bookDetails[key] ? bookDetails[key].split(',').map(genre => genre.trim()) : []
|
||||
} else if (!bookDetails[key]) { // Use null for empty details
|
||||
bookDetails[key] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Chapter support
|
||||
|
||||
return {
|
||||
book: bookDetails
|
||||
}
|
||||
}
|
||||
module.exports.parse = parseAbMetadataText
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
const fs = require('fs-extra')
|
||||
const filePerms = require('./filePerms')
|
||||
const package = require('../../package.json')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const bookKeyMap = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
author: 'authorFL',
|
||||
narrator: 'narratorFL',
|
||||
series: 'series',
|
||||
volumeNumber: 'volumeNumber',
|
||||
publishYear: 'publishYear',
|
||||
publisher: 'publisher',
|
||||
description: 'description',
|
||||
isbn: 'isbn',
|
||||
asin: 'asin',
|
||||
language: 'language',
|
||||
genres: 'genresCommaSeparated'
|
||||
}
|
||||
|
||||
function generate(audiobook, outputPath, uid, gid) {
|
||||
var fileString = `[audiobookshelf v${package.version}]\n`
|
||||
|
||||
for (const key in bookKeyMap) {
|
||||
const value = audiobook.book[bookKeyMap[key]] || ''
|
||||
fileString += `${key}=${value}\n`
|
||||
}
|
||||
|
||||
return fs.writeFile(outputPath, fileString).then(() => {
|
||||
return filePerms(outputPath, 0o774, uid, gid).then(() => true)
|
||||
}).catch((error) => {
|
||||
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
module.exports.generate = generate
|
||||
|
|
@ -6,11 +6,6 @@ module.exports.ScanResult = {
|
|||
UPTODATE: 4
|
||||
}
|
||||
|
||||
module.exports.CoverDestination = {
|
||||
METADATA: 0,
|
||||
AUDIOBOOK: 1
|
||||
}
|
||||
|
||||
module.exports.BookCoverAspectRatio = {
|
||||
STANDARD: 0, // 1.6:1
|
||||
SQUARE: 1
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const chmodr = (p, mode, uid, gid, cb) => {
|
|||
// any error other than ENOTDIR means it's not readable, or
|
||||
// doesn't exist. give up.
|
||||
if (er && er.code !== 'ENOTDIR') return cb(er)
|
||||
if (er) {
|
||||
if (er) { // Is a file
|
||||
return fs.chmod(p, mode).then(() => {
|
||||
fs.chown(p, uid, gid, cb)
|
||||
})
|
||||
|
|
@ -77,9 +77,9 @@ const chmodr = (p, mode, uid, gid, cb) => {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = (path, mode, uid, gid) => {
|
||||
module.exports = (path, mode, uid, gid, silent = false) => {
|
||||
return new Promise((resolve) => {
|
||||
Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||
chmodr(path, mode, uid, gid, resolve)
|
||||
})
|
||||
}
|
||||
|
|
@ -20,6 +20,23 @@ async function getFileStat(path) {
|
|||
}
|
||||
module.exports.getFileStat = getFileStat
|
||||
|
||||
async function getFileTimestampsWithIno(path) {
|
||||
try {
|
||||
var stat = await fs.stat(path, { bigint: true })
|
||||
return {
|
||||
size: Number(stat.size),
|
||||
mtimeMs: Number(stat.mtimeMs),
|
||||
ctimeMs: Number(stat.ctimeMs),
|
||||
birthtimeMs: Number(stat.birthtimeMs),
|
||||
ino: String(stat.ino)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to getFileTimestampsWithIno', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
|
||||
|
||||
async function getFileSize(path) {
|
||||
var stat = await getFileStat(path)
|
||||
if (!stat) return 0
|
||||
|
|
@ -27,6 +44,15 @@ async function getFileSize(path) {
|
|||
}
|
||||
module.exports.getFileSize = getFileSize
|
||||
|
||||
|
||||
function getIno(path) {
|
||||
return fs.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
}
|
||||
module.exports.getIno = getIno
|
||||
|
||||
async function readTextFile(path) {
|
||||
try {
|
||||
var data = await fs.readFile(path)
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@ module.exports.comparePaths = (path1, path2) => {
|
|||
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
|
||||
}
|
||||
|
||||
module.exports.getIno = (path) => {
|
||||
return fs.promises.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.isNullOrNaN = (num) => {
|
||||
return num === null || isNaN(num)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
const ffprobe = require('node-ffprobe')
|
||||
|
||||
if (process.env.FFPROBE_PATH) {
|
||||
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
||||
}
|
||||
|
||||
const AudioProbeData = require('../scanner/AudioProbeData')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
|
|
@ -281,6 +276,10 @@ function parseProbeData(data, verbose = false) {
|
|||
|
||||
// Updated probe returns AudioProbeData object
|
||||
function probe(filepath, verbose = false) {
|
||||
if (process.env.FFPROBE_PATH) {
|
||||
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
||||
}
|
||||
|
||||
return ffprobe(filepath)
|
||||
.then(raw => {
|
||||
var rawProbeData = parseProbeData(raw, verbose)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const Logger = require('../Logger')
|
||||
const { getIno } = require('./index')
|
||||
const { recurseFiles } = require('./fileUtils')
|
||||
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
|
||||
const globals = require('./globals')
|
||||
|
||||
function isBookFile(path) {
|
||||
|
|
@ -114,16 +113,20 @@ function groupFileItemsIntoBooks(fileItems) {
|
|||
}
|
||||
|
||||
function cleanFileObjects(basepath, abrelpath, files) {
|
||||
return files.map((file) => {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
var fullPath = Path.posix.join(basepath, file)
|
||||
var fileTsData = await getFileTimestampsWithIno(fullPath)
|
||||
|
||||
var ext = Path.extname(file)
|
||||
return {
|
||||
filetype: getFileType(ext),
|
||||
filename: Path.basename(file),
|
||||
path: Path.posix.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3
|
||||
fullPath: Path.posix.join(basepath, file), // /audiobooks/AUDIOBOOK/PATH/filename.mp3
|
||||
ext: ext
|
||||
fullPath, // /audiobooks/AUDIOBOOK/PATH/filename.mp3
|
||||
ext: ext,
|
||||
...fileTsData
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function getFileType(ext) {
|
||||
|
|
@ -162,15 +165,15 @@ async function scanRootDir(folder, serverSettings = {}) {
|
|||
for (const audiobookPath in audiobookGrouping) {
|
||||
var audiobookData = getAudiobookDataFromDir(folderPath, audiobookPath, parseSubtitle)
|
||||
|
||||
var fileObjs = cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
|
||||
for (let i = 0; i < fileObjs.length; i++) {
|
||||
fileObjs[i].ino = await getIno(fileObjs[i].fullPath)
|
||||
}
|
||||
var audiobookIno = await getIno(audiobookData.fullPath)
|
||||
var fileObjs = await cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
|
||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
||||
audiobooks.push({
|
||||
folderId: folder.id,
|
||||
libraryId: folder.libraryId,
|
||||
ino: audiobookIno,
|
||||
ino: audiobookFolderStats.ino,
|
||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
||||
...audiobookData,
|
||||
audioFiles: fileObjs.filter(f => f.filetype === 'audio'),
|
||||
otherFiles: fileObjs.filter(f => f.filetype !== 'audio')
|
||||
|
|
@ -196,32 +199,42 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
|||
|
||||
|
||||
// If in a series directory check for volume number match
|
||||
/* ACCEPTS:
|
||||
/* ACCEPTS
|
||||
Book 2 - Title Here - Subtitle Here
|
||||
Title Here - Subtitle Here - Vol 12
|
||||
Title Here - volume 9 - Subtitle Here
|
||||
Vol. 3 Title Here - Subtitle Here
|
||||
1980 - Book 2-Title Here
|
||||
Title Here-Volume 999-Subtitle Here
|
||||
2 - Book Title
|
||||
100 - Book Title
|
||||
0.5 - Book Title
|
||||
*/
|
||||
var volumeNumber = null
|
||||
if (series) {
|
||||
// New volume regex to match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
|
||||
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
|
||||
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
|
||||
volumeNumber = volumeMatch[3]
|
||||
var replaceChunk = volumeMatch[2]
|
||||
// Added 1.7.1: If title starts with a # that is 3 digits or less (or w/ 2 decimal), then use as volume number
|
||||
var volumeMatch = title.match(/^(\d{1,3}(?:\.\d{1,2})?) - ./)
|
||||
if (volumeMatch && volumeMatch.length > 1) {
|
||||
volumeNumber = volumeMatch[1]
|
||||
title = title.replace(`${volumeNumber} - `, '')
|
||||
} else {
|
||||
// Match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
|
||||
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
|
||||
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
|
||||
volumeNumber = volumeMatch[3]
|
||||
var replaceChunk = volumeMatch[2]
|
||||
|
||||
// "1980 - Book 2-Title Here"
|
||||
// Group 1 would be "- "
|
||||
// Group 3 would be "-"
|
||||
// Only remove the first group
|
||||
if (volumeMatch[1]) {
|
||||
replaceChunk = volumeMatch[1] + replaceChunk
|
||||
} else if (volumeMatch[4]) {
|
||||
replaceChunk += volumeMatch[4]
|
||||
// "1980 - Book 2-Title Here"
|
||||
// Group 1 would be "- "
|
||||
// Group 3 would be "-"
|
||||
// Only remove the first group
|
||||
if (volumeMatch[1]) {
|
||||
replaceChunk = volumeMatch[1] + replaceChunk
|
||||
} else if (volumeMatch[4]) {
|
||||
replaceChunk += volumeMatch[4]
|
||||
}
|
||||
title = title.replace(replaceChunk, '').trim()
|
||||
}
|
||||
title = title.replace(replaceChunk, '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,8 +285,12 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
|
|||
|
||||
var audiobookDir = audiobookPath.replace(folderFullPath, '').slice(1)
|
||||
var audiobookData = getAudiobookDataFromDir(folderFullPath, audiobookDir, parseSubtitle)
|
||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
||||
var audiobook = {
|
||||
ino: await getIno(audiobookData.fullPath),
|
||||
ino: audiobookFolderStats.ino,
|
||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
||||
folderId: folder.id,
|
||||
libraryId: folder.libraryId,
|
||||
...audiobookData,
|
||||
|
|
@ -284,14 +301,14 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
|
|||
for (let i = 0; i < fileItems.length; i++) {
|
||||
var fileItem = fileItems[i]
|
||||
|
||||
var ino = await getIno(fileItem.fullpath)
|
||||
var fileStatData = await getFileTimestampsWithIno(fileItem.fullpath)
|
||||
var fileObj = {
|
||||
ino,
|
||||
filetype: getFileType(fileItem.extension),
|
||||
filename: fileItem.name,
|
||||
path: fileItem.path,
|
||||
fullPath: fileItem.fullpath,
|
||||
ext: fileItem.extension
|
||||
ext: fileItem.extension,
|
||||
...fileStatData
|
||||
}
|
||||
if (fileObj.filetype === 'audio') {
|
||||
audiobook.audioFiles.push(fileObj)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue