From 33dfb764fa8d233f76a4d9d1918fba794028fe0d Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 27 Apr 2022 19:42:34 -0500 Subject: [PATCH] Add:Support for openaudible folder structure (subject to change), add support for treating single audio files in the root directory as library items #401 --- client/components/cards/LazyBookCard.vue | 6 +- .../components/modals/item/tabs/Details.vue | 5 +- client/components/modals/item/tabs/Files.vue | 10 --- client/components/tables/TracksTable.vue | 5 +- client/components/widgets/AudiobookData.vue | 5 +- client/pages/audiobook/_id/edit.vue | 4 + client/pages/item/_id/index.vue | 5 +- server/Db.js | 4 +- server/controllers/LibraryItemController.js | 6 ++ server/managers/CoverManager.js | 2 +- server/objects/LibraryItem.js | 7 +- server/scanner/Scanner.js | 2 +- server/utils/fileUtils.js | 22 +++-- server/utils/scandir.js | 82 +++++++++++++------ 14 files changed, 110 insertions(+), 55 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 3bebacce..a642df71 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -150,6 +150,10 @@ export default { _libraryItem() { return this.libraryItem || {} }, + isFile() { + // Library item is not in a folder + return this._libraryItem.isFile + }, media() { return this._libraryItem.media || {} }, @@ -365,7 +369,7 @@ export default { text: 'Match' }) } - if (this.userIsRoot) { + if (this.userIsRoot && !this.isFile) { items.push({ func: 'rescan', text: 'Re-Scan' diff --git a/client/components/modals/item/tabs/Details.vue b/client/components/modals/item/tabs/Details.vue index 6aeb84a4..e5c3a3fe 100644 --- a/client/components/modals/item/tabs/Details.vue +++ b/client/components/modals/item/tabs/Details.vue @@ -14,7 +14,7 @@ - Re-Scan + Re-Scan Submit @@ -49,6 +49,9 @@ export default { this.$emit('update:processing', val) } }, + isFile() { + return !!this.libraryItem && this.libraryItem.isFile + }, isRootUser() { return this.$store.getters['user/getIsRoot'] }, diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index f74df8d7..386aa643 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -1,9 +1,5 @@ @@ -51,12 +47,6 @@ export default { }, showDownload() { return this.userCanDownload && !this.isMissing - }, - audiobooks() { - return this.media.audiobooks || [] - }, - ebooks() { - return this.media.ebooks || [] } }, methods: { diff --git a/client/components/tables/TracksTable.vue b/client/components/tables/TracksTable.vue index 4df9050f..89f366fd 100644 --- a/client/components/tables/TracksTable.vue +++ b/client/components/tables/TracksTable.vue @@ -8,7 +8,7 @@
- + Manage Tracks
@@ -59,7 +59,8 @@ export default { type: Array, default: () => [] }, - libraryItemId: String + libraryItemId: String, + isFile: Boolean }, data() { return { diff --git a/client/components/widgets/AudiobookData.vue b/client/components/widgets/AudiobookData.vue index a218c58e..b909c378 100644 --- a/client/components/widgets/AudiobookData.vue +++ b/client/components/widgets/AudiobookData.vue @@ -16,7 +16,7 @@
- + @@ -27,7 +27,8 @@ export default { media: { type: Object, default: () => {} - } + }, + isFile: Boolean }, data() { return {} diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index 8f2b8b97..7969932a 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -107,6 +107,10 @@ export default { console.error('Invalid media type') return redirect('/') } + if (libraryItem.isFile) { + console.error('No need to edit library item that is 1 file...') + return redirect('/') + } return { libraryItem, files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : [] diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 9e6f8be9..cd08df99 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -165,7 +165,7 @@

- {{ audioFile.metadata.filename }} ({{ audioFile.error }})

- + @@ -210,6 +210,9 @@ export default { } }, computed: { + isFile() { + return this.libraryItem.isFile + }, coverAspectRatio() { return this.$store.getters['getServerSetting']('coverAspectRatio') }, diff --git a/server/Db.js b/server/Db.js index b43594b8..2addf69b 100644 --- a/server/Db.js +++ b/server/Db.js @@ -411,7 +411,9 @@ class Db { removeEntity(entityName, entityId) { var entityDb = this.getEntityDb(entityName) - return entityDb.delete((record) => record.id === entityId).then((results) => { + return entityDb.delete((record) => { + return record.id === entityId + }).then((results) => { Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`) var arrayKey = this.getEntityArrayKey(entityName) if (this[arrayKey]) { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index b494c754..2189a2b4 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -342,6 +342,12 @@ class LibraryItemController { Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user) return res.sendStatus(403) } + + if (req.libraryItem.isFile) { + Logger.error(`[LibraryItemController] Re-scanning file library items not yet supported`) + return res.sendStatus(500) + } + var result = await this.scanner.scanLibraryItemById(req.libraryItem.id) res.json({ result: Object.keys(ScanResult).find(key => ScanResult[key] == result) diff --git a/server/managers/CoverManager.js b/server/managers/CoverManager.js index 0dfdfdce..20a53d89 100644 --- a/server/managers/CoverManager.js +++ b/server/managers/CoverManager.js @@ -19,7 +19,7 @@ class CoverManager { } getCoverDirectory(libraryItem) { - if (this.db.serverSettings.storeCoverWithItem) { + if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile) { return libraryItem.path } else { return Path.posix.join(this.ItemMetadataPath, libraryItem.id) diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index ed956757..74d13792 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -18,6 +18,7 @@ class LibraryItem { this.path = null this.relPath = null + this.isFile = false this.mtimeMs = null this.ctimeMs = null this.birthtimeMs = null @@ -51,6 +52,7 @@ class LibraryItem { this.folderId = libraryItem.folderId this.path = libraryItem.path this.relPath = libraryItem.relPath + this.isFile = !!libraryItem.isFile this.mtimeMs = libraryItem.mtimeMs || 0 this.ctimeMs = libraryItem.ctimeMs || 0 this.birthtimeMs = libraryItem.birthtimeMs || 0 @@ -82,6 +84,7 @@ class LibraryItem { folderId: this.folderId, path: this.path, relPath: this.relPath, + isFile: this.isFile, mtimeMs: this.mtimeMs, ctimeMs: this.ctimeMs, birthtimeMs: this.birthtimeMs, @@ -105,6 +108,7 @@ class LibraryItem { folderId: this.folderId, path: this.path, relPath: this.relPath, + isFile: this.isFile, mtimeMs: this.mtimeMs, ctimeMs: this.ctimeMs, birthtimeMs: this.birthtimeMs, @@ -128,6 +132,7 @@ class LibraryItem { folderId: this.folderId, path: this.path, relPath: this.relPath, + isFile: this.isFile, mtimeMs: this.mtimeMs, ctimeMs: this.ctimeMs, birthtimeMs: this.birthtimeMs, @@ -460,7 +465,7 @@ class LibraryItem { this.isSavingMetadata = true var metadataPath = Path.join(global.MetadataPath, 'items', this.id) - if (global.ServerSettings.storeMetadataWithItem) { + if (global.ServerSettings.storeMetadataWithItem && !this.isFile) { metadataPath = this.path } else { // Make sure metadata book dir exists diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 19fd3c99..d7c62091 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -235,7 +235,7 @@ class Scanner { var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile) if (!hasMediaFile) { - libraryScan.addLog(LogLevel.WARN, `Directory found "${libraryItemDataFound.path}" has no media files`) + libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`) } else { var audioFileSize = 0 dataFound.libraryFiles.filter(lf => lf.fileType == 'audio').forEach(lf => audioFileSize += lf.metadata.size) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 763a579c..3cf98f06 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -115,6 +115,7 @@ async function recurseFiles(path, relPathToReplace = null) { var relpath = item.fullname.replace(relPathToReplace, '') var reldirname = Path.dirname(relpath) + if (reldirname === '.') reldirname = '' var dirname = Path.dirname(item.fullname) // Directory has a file named ".ignore" flag directory and ignore @@ -139,15 +140,18 @@ async function recurseFiles(path, relPathToReplace = null) { return false } return true - }).map((item) => ({ - name: item.name, - path: item.fullname.replace(relPathToReplace, ''), - dirpath: item.path, - reldirpath: item.path.replace(relPathToReplace, ''), - fullpath: item.fullname, - extension: item.extension, - deep: item.deep - })) + }).map((item) => { + var isInRoot = (item.path + '/' === relPathToReplace) + return { + name: item.name, + path: item.fullname.replace(relPathToReplace, ''), + dirpath: item.path, + reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''), + fullpath: item.fullname, + extension: item.extension, + deep: item.deep + } + }) // Sort from least deep to most list.sort((a, b) => a.deep - b.deep) diff --git a/server/utils/scandir.js b/server/utils/scandir.js index b7672c1e..3bcfbc35 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -5,9 +5,9 @@ const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils') const globals = require('./globals') const LibraryFile = require('../objects/files/LibraryFile') -function isMediaFile(mediaType, path) { - if (!path) return false - var ext = Path.extname(path) +function isMediaFile(mediaType, ext) { + // if (!path) return false + // var ext = Path.extname(path) if (!ext) return false var extclean = ext.slice(1).toLowerCase() if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean) @@ -62,40 +62,47 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths // Input: array of relative file items (see recurseFiles) // Output: map of files grouped into potential libarary item dirs function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { - // Step 1: Filter out files in root dir (with depth of 0) - var itemsFiltered = fileItems.filter(i => i.deep > 0) + // Step 1: Filter out non-media files in root dir (with depth of 0) + var itemsFiltered = fileItems.filter(i => { + return i.deep > 0 || isMediaFile(mediaType, i.extension) + }) // Step 2: Seperate media files and other files // - Directories without a media file will not be included var mediaFileItems = [] var otherFileItems = [] itemsFiltered.forEach(item => { - if (isMediaFile(mediaType, item.fullpath)) mediaFileItems.push(item) + if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item) else otherFileItems.push(item) }) // Step 3: Group audio files in library items var libraryItemGroup = {} mediaFileItems.forEach((item) => { - var dirparts = item.reldirpath.split('/') + var dirparts = item.reldirpath.split('/').filter(p => !!p) var numparts = dirparts.length var _path = '' - // Iterate over directories in path - for (let i = 0; i < numparts; i++) { - var dirpart = dirparts.shift() - _path = Path.posix.join(_path, dirpart) + if (!dirparts.length) { + // Media file in root + libraryItemGroup[item.name] = item.name + } else { + // Iterate over directories in path + for (let i = 0; i < numparts; i++) { + var dirpart = dirparts.shift() + _path = Path.posix.join(_path, dirpart) - if (libraryItemGroup[_path]) { // Directory already has files, add file - var relpath = Path.posix.join(dirparts.join('/'), item.name) - libraryItemGroup[_path].push(relpath) - return - } else if (!dirparts.length) { // This is the last directory, create group - libraryItemGroup[_path] = [item.name] - return - } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group - libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] - return + if (libraryItemGroup[_path]) { // Directory already has files, add file + var relpath = Path.posix.join(dirparts.join('/'), item.name) + libraryItemGroup[_path].push(relpath) + return + } else if (!dirparts.length) { // This is the last directory, create group + libraryItemGroup[_path] = [item.name] + return + } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group + libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] + return + } } } }) @@ -140,6 +147,15 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { } var fileItems = await recurseFiles(folderPath) + var basePath = folderPath + + const isOpenAudibleFolder = fileItems.find(fi => fi.deep === 0 && fi.name === 'books.json') + if (isOpenAudibleFolder) { + Logger.info(`[scandir] Detected Open Audible Folder, looking in books folder`) + basePath = Path.posix.join(folderPath, 'books') + fileItems = await recurseFiles(basePath) + Logger.debug(`[scandir] ${fileItems.length} files found in books folder`) + } var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) @@ -148,11 +164,27 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { return [] } + var isFile = false // item is not in a folder var items = [] for (const libraryItemPath in libraryItemGrouping) { - var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) + var libraryItemData = null + var fileObjs = [] + if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { + // Media file in root only get title + libraryItemData = { + mediaMetadata: { + title: Path.basename(libraryItemPath, Path.extname(libraryItemPath)) + }, + path: Path.posix.join(basePath, libraryItemPath), + relPath: libraryItemPath + } + fileObjs = await cleanFileObjects(basePath, [libraryItemPath]) + isFile = true + } else { + libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) + fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) + } - var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) items.push({ folderId: folder.id, @@ -163,6 +195,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { birthtimeMs: libraryItemFolderStats.birthtimeMs || 0, path: libraryItemData.path, relPath: libraryItemData.relPath, + isFile, media: { metadata: libraryItemData.mediaMetadata || null }, @@ -242,7 +275,6 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) { } } - // Subtitle can be parsed from the title if user enabled // Subtitle is everything after " - " var subtitle = null @@ -290,7 +322,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin } } - +// Called from Scanner.js async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) { var fileItems = await recurseFiles(libraryItemPath)