mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27:01 -04:00 
			
		
		
		
	Add:Support for openaudible folder structure (subject to change), add support for treating single audio files in the root directory as library items #401
This commit is contained in:
		
							parent
							
								
									49bef2c641
								
							
						
					
					
						commit
						33dfb764fa
					
				| @ -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' | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
|         </ui-tooltip> | ||||
| 
 | ||||
|         <ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4"> | ||||
|           <ui-btn v-if="isRootUser" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn> | ||||
|           <ui-btn v-if="isRootUser && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn> | ||||
|         </ui-tooltip> | ||||
| 
 | ||||
|         <ui-btn @click="submitForm">Submit</ui-btn> | ||||
| @ -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'] | ||||
|     }, | ||||
|  | ||||
| @ -1,9 +1,5 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> | ||||
|     <template v-for="audiobook in audiobooks"> | ||||
|       <tables-tracks-table :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :audiobook-id="audiobook.id" :tracks="audiobook.tracks" class="mb-4" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <tables-library-files-table expanded :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" /> | ||||
|   </div> | ||||
| </template> | ||||
| @ -51,12 +47,6 @@ export default { | ||||
|     }, | ||||
|     showDownload() { | ||||
|       return this.userCanDownload && !this.isMissing | ||||
|     }, | ||||
|     audiobooks() { | ||||
|       return this.media.audiobooks || [] | ||||
|     }, | ||||
|     ebooks() { | ||||
|       return this.media.ebooks || [] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|       <!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> --> | ||||
|       <div class="flex-grow" /> | ||||
|       <ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn> | ||||
|       <nuxt-link v-if="userCanUpdate" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> | ||||
|       <nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> | ||||
|         <ui-btn small color="primary">Manage Tracks</ui-btn> | ||||
|       </nuxt-link> | ||||
|       <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''"> | ||||
| @ -59,7 +59,8 @@ export default { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     }, | ||||
|     libraryItemId: String | ||||
|     libraryItemId: String, | ||||
|     isFile: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :library-item-id="libraryItemId" class="mt-6" /> | ||||
|     <tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -27,7 +27,8 @@ export default { | ||||
|     media: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|     }, | ||||
|     isFile: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return {} | ||||
|  | ||||
| @ -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 })) : [] | ||||
|  | ||||
| @ -165,7 +165,7 @@ | ||||
|             <p v-for="audioFile in invalidAudioFiles" :key="audioFile.id" class="text-xs pl-2">- {{ audioFile.metadata.filename }} ({{ audioFile.error }})</p> | ||||
|           </div> | ||||
| 
 | ||||
|           <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" /> | ||||
|           <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" /> | ||||
| 
 | ||||
|           <tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" /> | ||||
| 
 | ||||
| @ -210,6 +210,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     isFile() { | ||||
|       return this.libraryItem.isFile | ||||
|     }, | ||||
|     coverAspectRatio() { | ||||
|       return this.$store.getters['getServerSetting']('coverAspectRatio') | ||||
|     }, | ||||
|  | ||||
| @ -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]) { | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user