@@ -127,7 +136,9 @@ export default {
showUploader: true,
validAudioFiles: [],
validImageFiles: [],
- invalidFiles: []
+ invalidFiles: [],
+ selectedLibraryId: null,
+ selectedFolderId: null
}
},
computed: {
@@ -140,13 +151,55 @@ export default {
directory() {
if (!this.author || !this.title) return ''
if (this.series) {
- return Path.join('/audiobooks', this.author, this.series, this.title)
+ return Path.join(this.author, this.series, this.title)
} else {
- return Path.join('/audiobooks', this.author, this.title)
+ return Path.join(this.author, this.title)
}
+ },
+ libraries() {
+ return this.$store.state.libraries.libraries
+ },
+ libraryItems() {
+ return this.libraries.map((lib) => {
+ return {
+ value: lib.id,
+ text: lib.name
+ }
+ })
+ },
+ selectedLibrary() {
+ return this.libraries.find((lib) => lib.id === this.selectedLibraryId)
+ },
+ selectedFolder() {
+ if (!this.selectedLibrary) return null
+ return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
+ },
+ folderItems() {
+ if (!this.selectedLibrary) return []
+ return this.selectedLibrary.folders.map((fold) => {
+ return {
+ value: fold.id,
+ text: fold.fullPath
+ }
+ })
}
},
methods: {
+ libraryChanged() {
+ if (!this.selectedLibrary && this.selectedFolderId) {
+ this.selectedFolderId = null
+ } else if (this.selectedFolderId) {
+ if (!this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)) {
+ this.selectedFolderId = null
+ }
+ }
+ this.setDefaultFolder()
+ },
+ setDefaultFolder() {
+ if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
+ this.selectedFolderId = this.selectedLibrary.folders[0].id
+ }
+ },
reset() {
this.title = ''
this.author = ''
@@ -218,12 +271,18 @@ export default {
this.$toast.error('Must enter a title and author')
return
}
+ if (!this.selectedLibraryId || !this.selectedFolderId) {
+ this.$toast.error('Must select a library and folder')
+ return
+ }
this.processing = true
var form = new FormData()
form.set('title', this.title)
form.set('author', this.author)
form.set('series', this.series)
+ form.set('library', this.selectedLibraryId)
+ form.set('folder', this.selectedFolderId)
var index = 0
var files = this.validAudioFiles.concat(this.validImageFiles)
@@ -234,21 +293,21 @@ export default {
this.$axios
.$post('/upload', form)
.then((data) => {
- if (data.error) {
- this.$toast.error(data.error)
- } else {
- this.$toast.success('Audiobook Uploaded Successfully')
- this.reset()
- }
+ this.$toast.success('Audiobook Uploaded Successfully')
+ this.reset()
this.processing = false
})
.catch((error) => {
console.error('Failed', error)
- this.$toast.error('Oops, something went wrong...')
+ var errorMessage = error.response && error.response.data ? error.response.data : 'Oops, something went wrong...'
+ this.$toast.error(errorMessage)
this.processing = false
})
}
},
- mounted() {}
+ mounted() {
+ this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
+ this.setDefaultFolder()
+ }
}
\ No newline at end of file
diff --git a/client/store/index.js b/client/store/index.js
index a91a90ff..f0bf5ab6 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -35,7 +35,7 @@ export const actions = {
}
return this.$axios.$patch('/api/serverSettings', updatePayload).then((result) => {
if (result.success) {
- commit('setServerSettings', result.settings)
+ commit('setServerSettings', result.serverSettings)
return true
} else {
return false
@@ -78,6 +78,7 @@ export const mutations = {
state.versionData = versionData
},
setServerSettings(state, settings) {
+ if (!settings) return
state.serverSettings = settings
},
setStreamAudiobook(state, audiobook) {
diff --git a/package.json b/package.json
index e9a8582c..3812a6a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.3.5",
+ "version": "1.4.0",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
diff --git a/server/ApiController.js b/server/ApiController.js
index ee18c68f..4a8fb777 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -557,7 +557,7 @@ class ApiController {
}
var settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
- return res.sendStatus(500)
+ return res.status(500).send('Invalid settings update object')
}
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
if (madeUpdates) {
diff --git a/server/BookFinder.js b/server/BookFinder.js
index 7e4ea0c4..fb7b45f4 100644
--- a/server/BookFinder.js
+++ b/server/BookFinder.js
@@ -7,6 +7,8 @@ class BookFinder {
constructor() {
this.openLibrary = new OpenLibrary()
this.libGen = new LibGen()
+
+ this.verbose = false
}
async findByISBN(isbn) {
@@ -92,17 +94,17 @@ class BookFinder {
return b
}).filter(b => {
if (b.includesTitle) { // If search title was found in result title then skip over leven distance check
- Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
+ if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
} else if (b.titleDistance > maxTitleDistance) {
- Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
+ if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
return false
}
if (author) {
if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check
- Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
+ if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
} else if (b.authorDistance > maxAuthorDistance) {
- Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
+ if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
return false
}
}
@@ -115,28 +117,28 @@ class BookFinder {
async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) {
var books = await this.libGen.search(title)
- Logger.debug(`LibGen Book Search Results: ${books.length || 0}`)
+ if (this.verbose) Logger.debug(`LibGen Book Search Results: ${books.length || 0}`)
if (books.errorCode) {
Logger.error(`LibGen Search Error ${books.errorCode}`)
return []
}
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
if (!booksFiltered.length && books.length) {
- Logger.debug(`Search has ${books.length} matches, but no close title matches`)
+ if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
}
return booksFiltered
}
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
var books = await this.openLibrary.searchTitle(title)
- Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
+ if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
if (books.errorCode) {
Logger.error(`OpenLib Search Error ${books.errorCode}`)
return []
}
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
if (!booksFiltered.length && books.length) {
- Logger.debug(`Search has ${books.length} matches, but no close title matches`)
+ if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
}
return booksFiltered
}
@@ -145,7 +147,7 @@ class BookFinder {
var books = []
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
- Logger.debug(`Book Search, title: "${title}", author: "${author}", provider: ${provider}`)
+ Logger.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`)
if (provider === 'libgen') {
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
diff --git a/server/CoverController.js b/server/CoverController.js
index 0489c4a9..b22761ef 100644
--- a/server/CoverController.js
+++ b/server/CoverController.js
@@ -117,7 +117,7 @@ class CoverController {
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
- audiobook.updateBookCover(coverPath)
+ audiobook.updateBookCover(coverPath, coverFullPath)
return {
cover: coverPath
}
@@ -169,7 +169,7 @@ class CoverController {
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
- audiobook.updateBookCover(coverPath)
+ audiobook.updateBookCover(coverPath, coverFullPath)
return {
cover: coverPath
}
diff --git a/server/Scanner.js b/server/Scanner.js
index 2efb9d72..efbb73f7 100644
--- a/server/Scanner.js
+++ b/server/Scanner.js
@@ -104,7 +104,32 @@ class Scanner {
return filesUpdated
}
- async scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, forceAudioFileScan) {
+ async searchForCover(audiobook) {
+ var options = {
+ titleDistance: 2,
+ authorDistance: 2
+ }
+ var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
+ if (results.length) {
+ Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
+
+ // If the first cover result fails, attempt to download the second
+ for (let i = 0; i < results.length && i < 2; i++) {
+
+ // Downloads and updates the book cover
+ var result = await this.coverController.downloadCoverFromUrl(audiobook, results[i])
+
+ if (result.error) {
+ Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
+ } else {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ async scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan) {
// Always sync files and inode values
var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData)
if (hasUpdatedIno || filesInodeUpdated > 0) {
@@ -204,7 +229,7 @@ class Scanner {
return ScanResult.REMOVED
}
- var hasUpdates = hasUpdatedIno || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
+ var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
// Check that audio tracks are in sequential order with no gaps
if (existingAudiobook.checkUpdateMissingParts()) {
@@ -230,18 +255,13 @@ class Scanner {
Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`)
}
- // Save changes and notify users
- if (hasUpdates || !existingAudiobook.scanVersion) {
- if (!existingAudiobook.scanVersion) {
- Logger.debug(`[Scanner] No scan version "${existingAudiobook.title}" - updating`)
- }
+ if (hasUpdates || version !== existingAudiobook.scanVersion) {
existingAudiobook.setChapters()
-
- Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
existingAudiobook.setLastScan(version)
await this.db.updateAudiobook(existingAudiobook)
- this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
+ Logger.info(`[Scanner] "${existingAudiobook.title}" was updated`)
+ this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
return ScanResult.UPDATED
}
@@ -251,7 +271,7 @@ class Scanner {
async scanNewAudiobook(audiobookData) {
if (!audiobookData.audioFiles.length) {
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
- return ScanResult.NOTHING
+ return null
}
var audiobook = new Audiobook()
@@ -261,13 +281,14 @@ class Scanner {
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
if (!audiobook.tracks.length) {
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
- return ScanResult.NOTHING
+ return null
}
// Look for desc.txt and reader.txt and update
await audiobook.saveDataFromTextFiles()
- if (audiobook.hasEmbeddedCoverArt) {
+ // Extract embedded cover art if cover is not already in directory
+ if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
var outputCoverDirs = this.getCoverDirectory(audiobook)
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
if (relativeDir) {
@@ -289,31 +310,104 @@ class Scanner {
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
await this.db.insertEntity('audiobook', audiobook)
this.emitter('audiobook_added', audiobook.toJSONMinified())
- return ScanResult.ADDED
+ return audiobook
}
async scanAudiobookData(audiobookData, forceAudioFileScan = false) {
var scannerFindCovers = this.db.serverSettings.scannerFindCovers
var libraryId = audiobookData.libraryId
- var audiobooksInLibrary = this.audiobooks.filter(ab => ab.libraryId === libraryId)
- var existingAudiobook = audiobooksInLibrary.find(a => a.ino === audiobookData.ino)
+ var folderId = audiobookData.folderId
+
+ var hasUpdatedLibraryOrFolder = false
+
+ var existingAudiobook = this.audiobooks.find(ab => ab.ino === audiobookData.ino)
+
+ // Make sure existing audiobook has the same library & folder id
+ if (existingAudiobook && (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId)) {
+ var existingAudiobookLibrary = this.db.libraries.find(lib => lib.id === existingAudiobook.libraryId)
+
+ if (!existingAudiobookLibrary) {
+ Logger.error(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library that no longer exists ${existingAudiobook.libraryId}`)
+ } else if (existingAudiobook.libraryId !== libraryId) {
+ Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library "${existingAudiobookLibrary.name}"`)
+ } else {
+ Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different folder "${existingAudiobook.folderId}" of library "${existingAudiobookLibrary.name}"`)
+ }
+
+ existingAudiobook.libraryId = libraryId
+ existingAudiobook.folderId = folderId
+ hasUpdatedLibraryOrFolder = true
+ Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
+ }
+
+ // var audiobooksInLibrary = this.audiobooks.filter(ab => ab.libraryId === libraryId)
+ // var existingAudiobook = audiobooksInLibrary.find(a => a.ino === audiobookData.ino)
// inode value may change when using shared drives, update inode if matching path is found
// Note: inode will not change on rename
var hasUpdatedIno = false
if (!existingAudiobook) {
// check an audiobook exists with matching path, then update inodes
- existingAudiobook = audiobooksInLibrary.find(a => a.path === audiobookData.path)
+ existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path)
if (existingAudiobook) {
+ var oldIno = existingAudiobook.ino
existingAudiobook.ino = audiobookData.ino
+ Logger.debug(`[Scanner] Scan Audiobook Data: Updated inode from "${oldIno}" to "${existingAudiobook.ino}"`)
hasUpdatedIno = true
+
+ if (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId) {
+ Logger.warn(`[Scanner] Audiobook found by path is in a different library or folder, ${existingAudiobook.libraryId}/${existingAudiobook.folderId} should be ${libraryId}/${folderId}`)
+
+ existingAudiobook.libraryId = libraryId
+ existingAudiobook.folderId = folderId
+ hasUpdatedLibraryOrFolder = true
+ Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
+ }
}
}
+ var scanResult = null
+ var finalAudiobook = null
+
if (existingAudiobook) {
- return this.scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, forceAudioFileScan)
+ finalAudiobook = existingAudiobook
+
+ scanResult = await this.scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan)
+
+ if (scanResult === ScanResult.REMOVED || scanResult === ScanResult.NOTHING) {
+ finalAudiobook = null
+ }
+ } else {
+ finalAudiobook = await this.scanNewAudiobook(audiobookData)
+
+ scanResult = finalAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
+
+ if (finalAudiobook === ScanResult.NOTHING) {
+ finalAudiobook = null
+ scanResult = ScanResult.NOTHING
+ } else {
+ scanResult = ScanResult.ADDED
+ }
}
- return this.scanNewAudiobook(audiobookData)
+
+ // Scan for cover if enabled and has no cover
+ if (finalAudiobook && scannerFindCovers && !finalAudiobook.cover) {
+ if (finalAudiobook.book.shouldSearchForCover) {
+ var updatedCover = await this.searchForCover(finalAudiobook)
+
+ finalAudiobook.book.updateLastCoverSearch(updatedCover)
+
+ if (updatedCover && scanResult === ScanResult.UPTODATE) {
+ scanResult = ScanResult.UPDATED
+ }
+ await this.db.updateAudiobook(finalAudiobook)
+ this.emitter('audiobook_updated', finalAudiobook.toJSONMinified())
+ } else {
+ Logger.debug(`[Scanner] Audiobook "${finalAudiobook.title}" cover already scanned - not re-scanning`)
+ }
+ }
+
+ return scanResult
}
async scan(libraryId, forceAudioFileScan = false) {
@@ -331,15 +425,19 @@ class Scanner {
return
}
- this.emitter('scan_start', {
+ var scanPayload = {
id: libraryId,
name: library.name,
scanType: 'library',
folders: library.folders.length
- })
+ }
+ this.emitter('scan_start', scanPayload)
Logger.info(`[Scanner] Starting scan of library "${library.name}" with ${library.folders.length} folders`)
- this.librariesScanning.push(libraryId)
+ library.lastScan = Date.now()
+ await this.db.updateEntity('library', library)
+
+ this.librariesScanning.push(scanPayload)
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
@@ -351,7 +449,11 @@ class Scanner {
// Update ino if inos are not set
var shouldUpdateIno = ab.hasMissingIno
if (shouldUpdateIno) {
- Logger.debug(`Updating inos for ${ab.title}`)
+ var filesWithMissingIno = ab.getFilesWithMissingIno()
+
+ Logger.debug(`\n\Updating inos for "${ab.title}"`)
+ Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno)
+
var hasUpdates = await ab.checkUpdateInos()
if (hasUpdates) {
await this.db.updateAudiobook(ab)
@@ -373,10 +475,9 @@ class Scanner {
audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
if (this.cancelLibraryScan[libraryId]) {
- console.log('2', this.cancelLibraryScan)
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
delete this.cancelLibraryScan[libraryId]
- this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
+ this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: null })
return null
}
@@ -401,10 +502,9 @@ class Scanner {
this.emitter('audiobook_updated', audiobook.toJSONMinified())
}
if (this.cancelLibraryScan[libraryId]) {
- console.log('1', this.cancelLibraryScan)
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
delete this.cancelLibraryScan[libraryId]
- this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
+ this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
return
}
@@ -429,7 +529,6 @@ class Scanner {
}
})
if (this.cancelLibraryScan[libraryId]) {
- console.log(this.cancelLibraryScan)
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
delete this.cancelLibraryScan[libraryId]
break
@@ -437,7 +536,7 @@ class Scanner {
}
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`)
- this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
+ this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
}
@@ -457,6 +556,10 @@ class Scanner {
Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`)
return ScanResult.NOTHING
}
+ if (!folder.libraryId) {
+ Logger.fatal(`[Scanner] Folder does not have a library id set...`, folder)
+ return ScanResult.NOTHING
+ }
Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`)
return this.scanAudiobook(folder, audiobook.fullPath, true)
@@ -525,6 +628,7 @@ class Scanner {
Logger.error(`[Scanner] Folder "${folderId}" not found in library "${library.name}" for scan library updates`)
return null
}
+
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
var bookGroupingResults = {}
@@ -594,6 +698,22 @@ class Scanner {
// Group files by book
for (const folderId in folderGroups) {
var libraryId = folderGroups[folderId].libraryId
+ var library = this.db.libraries.find(lib => lib.id === libraryId)
+ if (!library) {
+ Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
+ continue;
+ }
+ var folder = library.getFolderById(folderId)
+ if (!folder) {
+ Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
+
+ Logger.debug(`Looking at folders in library "${library.name}" for folderid ${folderId}`)
+ library.folders.forEach((fold) => {
+ Logger.debug(`Folder "${fold.id}" "${fold.fullPath}"`)
+ })
+ continue;
+ }
+
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true)
var folderScanResults = await this.scanFolderUpdates(libraryId, folderId, fileUpdateBookGroup)
@@ -602,18 +722,6 @@ class Scanner {
Logger.debug(`[Scanner] Finished scanning file changes, results:`, libraryScanResults)
return libraryScanResults
- // var relfilepaths = filepaths.map(path => path.replace(this.AudiobookPath, ''))
- // var fileGroupings = groupFilesIntoAudiobookPaths(relfilepaths, true)
-
- // var results = []
- // for (const dir in fileGroupings) {
- // Logger.debug(`[Scanner] Check dir ${dir}`)
- // var fullPath = Path.join(this.AudiobookPath, dir)
- // var result = await this.checkDir(fullPath)
- // Logger.debug(`[Scanner] Check dir result ${result}`)
- // results.push(result)
- // }
- // return results
}
async scanCovers() {
diff --git a/server/Server.js b/server/Server.js
index a79c4b89..cd964b3f 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -178,8 +178,6 @@ class Server {
res.json({ success: true })
})
- app.get('/test-fs', this.authMiddleware.bind(this), this.testFileSystem.bind(this))
-
// Used in development to set-up streams without authentication
if (process.env.NODE_ENV !== 'production') {
app.use('/test-hls', this.hlsController.router)
@@ -292,7 +290,7 @@ class Server {
}
cancelScan(id) {
- console.log('Cancel scan', id)
+ Logger.debug('[Server] Cancel scan', id)
this.scanner.cancelLibraryScan[id] = true
}
@@ -344,26 +342,33 @@ class Server {
var title = req.body.title
var author = req.body.author
var series = req.body.series
+ var libraryId = req.body.library
+ var folderId = req.body.folder
+
+ var library = this.db.libraries.find(lib => lib.id === libraryId)
+ if (!library) {
+ return res.status(500).error(`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}`)
+ }
if (!files.length || !title || !author) {
- return res.json({
- error: 'Invalid post data received'
- })
+ return res.status(500).error(`Invalid post data`)
}
var outputDirectory = ''
if (series && series.length && series !== 'null') {
- outputDirectory = Path.join(this.AudiobookPath, author, series, title)
+ outputDirectory = Path.join(folder.fullPath, author, series, title)
} else {
- outputDirectory = Path.join(this.AudiobookPath, author, title)
+ outputDirectory = Path.join(folder.fullPath, author, title)
}
var exists = await fs.pathExists(outputDirectory)
if (exists) {
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
- return res.json({
- error: `Directory "${outputDirectory}" already exists`
- })
+ return res.status(500).error(`Directory "${outputDirectory}" already exists`)
}
await fs.ensureDir(outputDirectory)
@@ -438,7 +443,8 @@ class Server {
metadataPath: this.MetadataPath,
configPath: this.ConfigPath,
user: client.user.toJSONForBrowser(),
- stream: client.stream || null
+ stream: client.stream || null,
+ librariesScanning: this.scanner.librariesScanning
}
client.socket.emit('init', initialPayload)
@@ -463,26 +469,5 @@ class Server {
})
})
}
-
- async testFileSystem(req, res) {
- Logger.debug(`[Server] Running fs test`)
- var paths = await fs.readdir(global.appRoot)
- Logger.debug(paths)
- var pathMap = {}
- if (paths && paths.length) {
- for (let i = 0; i < paths.length; i++) {
- var fullPath = Path.join(global.appRoot, paths[i])
- Logger.debug('Checking path', fullPath)
- var isDirectory = fs.lstatSync(fullPath).isDirectory()
- if (isDirectory) {
- var _paths = await fs.readdir(fullPath)
- Logger.debug(_paths)
- pathMap[paths[i]] = _paths
- }
- }
- }
- Logger.debug('Finished fs test')
- res.json(pathMap)
- }
}
module.exports = Server
\ No newline at end of file
diff --git a/server/Watcher.js b/server/Watcher.js
index 1948d00a..3755dd0c 100644
--- a/server/Watcher.js
+++ b/server/Watcher.js
@@ -45,9 +45,9 @@ class FolderWatcher extends EventEmitter {
}).on('rename', (path, pathNext) => {
this.onRename(library.id, path, pathNext)
}).on('error', (error) => {
- Logger.error(`[FolderWatcher] ${error}`)
+ Logger.error(`[Watcher] ${error}`)
}).on('ready', () => {
- Logger.info('[FolderWatcher] Ready')
+ Logger.info(`[Watcher] "${library.name}" Ready`)
})
this.libraryWatchers.push({
@@ -107,18 +107,6 @@ class FolderWatcher extends EventEmitter {
onFileRemoved(libraryId, path) {
Logger.debug('[Watcher] File Removed', path)
this.addFileUpdate(libraryId, path, 'deleted')
- // var dir = Path.dirname(path)
- // if (dir === this.AudiobookPath) {
- // Logger.debug('New File added to root dir, ignoring it')
- // return
- // }
-
- // this.pendingFiles.push(path)
- // clearTimeout(this.pendingTimeout)
- // this.pendingTimeout = setTimeout(() => {
- // this.emit('files', this.pendingFiles.map(f => f))
- // this.pendingFiles = []
- // }, this.pendingDelay)
}
onFileUpdated(path) {
@@ -128,18 +116,6 @@ class FolderWatcher extends EventEmitter {
onRename(libraryId, pathFrom, pathTo) {
Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
this.addFileUpdate(libraryId, pathTo, 'renamed')
- // var dir = Path.dirname(pathTo)
- // if (dir === this.AudiobookPath) {
- // Logger.debug('New File added to root dir, ignoring it')
- // return
- // }
-
- // this.pendingFiles.push(pathTo)
- // clearTimeout(this.pendingTimeout)
- // this.pendingTimeout = setTimeout(() => {
- // this.emit('files', this.pendingFiles.map(f => f))
- // this.pendingFiles = []
- // }, this.pendingDelay)
}
addFileUpdate(libraryId, path, type) {
@@ -167,7 +143,7 @@ class FolderWatcher extends EventEmitter {
}
var relPath = path.replace(folder.fullPath, '')
- Logger.debug(`[Watcher] New File in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
+ Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
this.pendingFileUpdates.push({
path,
diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js
index 24eda5f4..f64458fd 100644
--- a/server/objects/Audiobook.js
+++ b/server/objects/Audiobook.js
@@ -118,6 +118,7 @@ class Audiobook {
get _audioFiles() { return this.audioFiles || [] }
get _otherFiles() { return this.otherFiles || [] }
+ get _tracks() { return this.tracks || [] }
get ebooks() {
return this.otherFiles.filter(file => file.filetype === 'ebook')
@@ -128,13 +129,21 @@ class Audiobook {
}
get hasMissingIno() {
- return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || (this.tracks || []).find(t => !t.ino)
+ return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
}
get hasEmbeddedCoverArt() {
return !!this._audioFiles.find(af => af.embeddedCoverArt)
}
+ // TEMP: Issue with inodes not always being set for files
+ getFilesWithMissingIno() {
+ var afs = this._audioFiles.filter(af => !af.ino)
+ var ofs = this._otherFiles.filter(f => !f.ino)
+ var ts = this._tracks.filter(t => !t.ino)
+ return afs.concat(ofs).concat(ts)
+ }
+
bookToJSON() {
return this.book ? this.book.toJSON() : null
}
@@ -332,8 +341,9 @@ class Audiobook {
if (this.otherFiles && this.otherFiles.length) {
var imageFile = this.otherFiles.find(f => f.filetype === 'image')
if (imageFile) {
- data.coverFullPath = imageFile.fullPath
- data.cover = Path.normalize(Path.join(`/s/book/${this.id}`, imageFile.path))
+ data.coverFullPath = Path.normalize(imageFile.fullPath)
+ var relImagePath = imageFile.path.replace(this.path, '')
+ data.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath))
}
}
@@ -387,9 +397,9 @@ class Audiobook {
}
// Cover Url may be the same, this ensures the lastUpdate is updated
- updateBookCover(cover) {
+ updateBookCover(cover, coverFullPath) {
if (!this.book) return false
- return this.book.updateCover(cover)
+ return this.book.updateCover(cover, coverFullPath)
}
updateAudioTracks(orderedFileData) {
@@ -479,7 +489,7 @@ class Audiobook {
}
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
- var descriptionTxt = this.otherFiles.find(file => file.filename === 'desc.txt')
+ var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
var newDescription = await readTextFile(descriptionTxt.fullPath)
if (newDescription) {
@@ -489,7 +499,7 @@ class Audiobook {
}
}
// If reader.txt is new or forcing rescan then read it and update narrarator (will overwrite)
- var readerTxt = this.otherFiles.find(file => file.filename === 'reader.txt')
+ var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
var newReader = await readTextFile(readerTxt.fullPath)
if (newReader) {
@@ -523,7 +533,7 @@ class Audiobook {
var oldFormat = this.book.cover
// Update book cover path to new format
- this.book.fullCoverPath = Path.join(this.fullPath, this.book.cover.substr(7))
+ this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(7)))
this.book.cover = Path.normalize(coverStripped.replace(this.path, `/s/book/${this.id}`))
Logger.debug(`[Audiobook] updated book cover to new format "${oldFormat}" => "${this.book.cover}"`)
}
@@ -531,10 +541,10 @@ class Audiobook {
}
// Check if book was removed from book dir
- if (this.book.cover && this.book.cover.substr(1).startsWith('s/book/')) {
+ if (this.book.cover && this.book.cover.substr(1).startsWith('s\\book\\')) {
// Fixing old cover paths
if (!this.book.coverFullPath) {
- this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length))
+ this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length)))
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
hasUpdates = true
}
@@ -550,7 +560,7 @@ class Audiobook {
if (this.book.cover && this.book.cover.substr(1).startsWith('metadata')) {
// Fixing old cover paths
if (!this.book.coverFullPath) {
- this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length))
+ this.book.coverFullPath = Path.normalize(Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)))
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
hasUpdates = true
}
@@ -575,11 +585,12 @@ class Audiobook {
// If no cover set and image file exists then use it
if (!this.book.cover && imageFiles.length) {
var imagePathRelativeToBook = imageFiles[0].path.replace(this.path, '')
- this.book.cover = Path.join(`/s/book/${this.id}`, imagePathRelativeToBook)
+ this.book.cover = Path.normalize(Path.join(`/s/book/${this.id}`, imagePathRelativeToBook))
this.book.coverFullPath = imageFiles[0].fullPath
Logger.info(`[Audiobook] Local cover was set to "${this.book.cover}" | "${this.title}"`)
hasUpdates = true
}
+
return hasUpdates
}
diff --git a/server/objects/Book.js b/server/objects/Book.js
index c195a896..f713e670 100644
--- a/server/objects/Book.js
+++ b/server/objects/Book.js
@@ -20,8 +20,14 @@ class Book {
this.cover = null
this.coverFullPath = null
this.genres = []
+
this.lastUpdate = null
+ // Should not continue looking up a cover when it is not findable
+ this.lastCoverSearch = null
+ this.lastCoverSearchTitle = null
+ this.lastCoverSearchAuthor = null
+
if (book) {
this.construct(book)
}
@@ -33,6 +39,12 @@ class Book {
get _author() { return this.author || '' }
get _series() { return this.series || '' }
+ get shouldSearchForCover() {
+ if (this.author !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
+ var timeSinceLastSearch = Date.now() - this.lastCoverSearch
+ return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
+ }
+
construct(book) {
this.olid = book.olid
this.title = book.title
@@ -50,6 +62,9 @@ class Book {
this.coverFullPath = book.coverFullPath || null
this.genres = book.genres
this.lastUpdate = book.lastUpdate || Date.now()
+ this.lastCoverSearch = book.lastCoverSearch || null
+ this.lastCoverSearchTitle = book.lastCoverSearchTitle || null
+ this.lastCoverSearchAuthor = book.lastCoverSearchAuthor || null
}
toJSON() {
@@ -69,7 +84,10 @@ class Book {
cover: this.cover,
coverFullPath: this.coverFullPath,
genres: this.genres,
- lastUpdate: this.lastUpdate
+ lastUpdate: this.lastUpdate,
+ lastCoverSearch: this.lastCoverSearch,
+ lastCoverSearchTitle: this.lastCoverSearchTitle,
+ lastCoverSearchAuthor: this.lastCoverSearchAuthor
}
}
@@ -106,6 +124,9 @@ class Book {
this.coverFullPath = data.coverFullPath || null
this.genres = data.genres || []
this.lastUpdate = Date.now()
+ this.lastCoverSearch = data.lastCoverSearch || null
+ this.lastCoverSearchTitle = data.lastCoverSearchTitle || null
+ this.lastCoverSearchAuthor = data.lastCoverSearchAuthor || null
if (data.author) {
this.setParseAuthor(this.author)
@@ -119,6 +140,7 @@ class Book {
// If updating to local cover then normalize path
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
payload.cover = Path.normalize(payload.cover)
+ if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath)
}
}
@@ -154,10 +176,19 @@ class Book {
return hasUpdates
}
- updateCover(cover) {
+ updateLastCoverSearch(coverWasFound) {
+ this.lastCoverSearch = coverWasFound ? null : Date.now()
+ this.lastCoverSearchAuthor = coverWasFound ? null : this.author
+ this.lastCoverSearchTitle = coverWasFound ? null : this.title
+ }
+
+ updateCover(cover, coverFullPath) {
if (!cover) return false
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
cover = Path.normalize(cover)
+ this.coverFullPath = Path.normalize(coverFullPath)
+ } else {
+ this.coverFullPath = cover
}
this.cover = cover
this.lastUpdate = Date.now()
diff --git a/server/objects/Library.js b/server/objects/Library.js
index f3e5ad15..3e7b4d4f 100644
--- a/server/objects/Library.js
+++ b/server/objects/Library.js
@@ -6,6 +6,8 @@ class Library {
this.name = null
this.folders = []
+ this.lastScan = 0
+
this.createdAt = null
this.lastUpdate = null
@@ -74,6 +76,7 @@ class Library {
if (newFolders.length) {
newFolders.forEach((folderData) => {
+ folderData.libraryId = this.id
var newFolder = new Folder()
newFolder.setData(folderData)
this.folders.push(newFolder)
@@ -91,5 +94,9 @@ class Library {
checkFullPathInLibrary(fullPath) {
return this.folders.find(folder => fullPath.startsWith(folder.fullPath))
}
+
+ getFolderById(id) {
+ return this.folders.find(folder => folder.id === id)
+ }
}
module.exports = Library
\ No newline at end of file
diff --git a/server/utils/scandir.js b/server/utils/scandir.js
index 4d792e5d..4fbeaa42 100644
--- a/server/utils/scandir.js
+++ b/server/utils/scandir.js
@@ -107,6 +107,7 @@ function getFileType(ext) {
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
if (ext_cleaned === 'nfo') return 'info'
+ if (ext_cleaned === 'txt') return 'text'
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
return 'unknown'
@@ -243,6 +244,7 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
var audiobookDir = Path.normalize(audiobookPath).replace(folder.fullPath, '').slice(1)
var audiobookData = getAudiobookDataFromDir(folder.fullPath, audiobookDir, parseSubtitle)
var audiobook = {
+ ino: await getIno(audiobookData.fullPath),
folderId: folder.id,
libraryId: folder.libraryId,
...audiobookData,