mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:New API routes for library files and downloads
This commit is contained in:
		
							parent
							
								
									ea79948122
								
							
						
					
					
						commit
						019063e6f4
					
				| @ -36,10 +36,10 @@ | ||||
|           </div> | ||||
| 
 | ||||
|           <div v-if="showLocalCovers" class="flex items-center justify-center pb-2"> | ||||
|             <template v-for="cover in localCovers"> | ||||
|               <div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)"> | ||||
|             <template v-for="localCoverFile in localCovers"> | ||||
|               <div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)"> | ||||
|                 <div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }"> | ||||
|                   <covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|                   <covers-preview-cover :src="localCoverFile.localPath" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </template> | ||||
| @ -169,8 +169,8 @@ export default { | ||||
|       return this.libraryFiles | ||||
|         .filter((f) => f.fileType === 'image') | ||||
|         .map((file) => { | ||||
|           var _file = { ...file } | ||||
|           _file.localPath = `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}` | ||||
|           const _file = { ...file } | ||||
|           _file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}` | ||||
|           return _file | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @ -73,7 +73,7 @@ export default { | ||||
|       return items | ||||
|     }, | ||||
|     downloadUrl() { | ||||
|       return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.track.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}` | ||||
|       return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}/download?token=${this.userToken}` | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -45,7 +45,7 @@ export default { | ||||
|       return this.$store.getters['user/getIsAdminOrUp'] | ||||
|     }, | ||||
|     downloadUrl() { | ||||
|       return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.file.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}` | ||||
|       return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.file.ino}/download?token=${this.userToken}` | ||||
|     }, | ||||
|     contextMenuItems() { | ||||
|       const items = [] | ||||
|  | ||||
| @ -162,6 +162,8 @@ class Server { | ||||
| 
 | ||||
|     router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) | ||||
|     router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) | ||||
| 
 | ||||
|     // TODO: Deprecated as of 2.2.21 edge
 | ||||
|     router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router) | ||||
| 
 | ||||
|     // EBook static file routes
 | ||||
|  | ||||
| @ -6,6 +6,7 @@ const SocketAuthority = require('../SocketAuthority') | ||||
| const zipHelpers = require('../utils/zipHelpers') | ||||
| const { reqSupportsWebp, isNullOrNaN } = require('../utils/index') | ||||
| const { ScanResult } = require('../utils/constants') | ||||
| const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') | ||||
| 
 | ||||
| class LibraryItemController { | ||||
|   constructor() { } | ||||
| @ -529,19 +530,45 @@ class LibraryItemController { | ||||
|     res.json(toneData) | ||||
|   } | ||||
| 
 | ||||
|   async deleteLibraryFile(req, res) { | ||||
|     const libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.ino) | ||||
|     if (!libraryFile) { | ||||
|       Logger.error(`[LibraryItemController] Unable to delete library file. Not found. "${req.params.ino}"`) | ||||
|       return res.sendStatus(404) | ||||
|   /** | ||||
|    * GET api/items/:id/file/:fileid | ||||
|    *  | ||||
|    * @param {express.Request} req | ||||
|    * @param {express.Response} res  | ||||
|    */ | ||||
|   async getLibraryFile(req, res) { | ||||
|     const libraryFile = req.libraryFile | ||||
| 
 | ||||
|     if (global.XAccel) { | ||||
|       Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) | ||||
|       return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() | ||||
|     } | ||||
| 
 | ||||
|     // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
 | ||||
|     const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path)) | ||||
|     if (audioMimeType) { | ||||
|       res.setHeader('Content-Type', audioMimeType) | ||||
|     } | ||||
|     res.sendFile(libraryFile.metadata.path) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * DELETE api/items/:id/file/:fileid | ||||
|    *  | ||||
|    * @param {express.Request} req | ||||
|    * @param {express.Response} res  | ||||
|    */ | ||||
|   async deleteLibraryFile(req, res) { | ||||
|     const libraryFile = req.libraryFile | ||||
| 
 | ||||
|     Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${libraryFile.metadata.path}"`) | ||||
| 
 | ||||
|     await fs.remove(libraryFile.metadata.path).catch((error) => { | ||||
|       Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) | ||||
|     }) | ||||
|     req.libraryItem.removeLibraryFile(req.params.ino) | ||||
|     req.libraryItem.removeLibraryFile(req.params.fileid) | ||||
| 
 | ||||
|     if (req.libraryItem.media.removeFileWithInode(req.params.ino)) { | ||||
|     if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) { | ||||
|       // If book has no more media files then mark it as missing
 | ||||
|       if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) { | ||||
|         req.libraryItem.setMissing() | ||||
| @ -553,6 +580,42 @@ class LibraryItemController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * GET api/items/:id/file/:fileid/download | ||||
|    * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads | ||||
|    * @param {express.Request} req | ||||
|    * @param {express.Response} res  | ||||
|    */ | ||||
|   async downloadLibraryFile(req, res) { | ||||
|     const libraryFile = req.libraryFile | ||||
| 
 | ||||
|     if (!req.user.canDownload) { | ||||
|       Logger.error(`[LibraryItemController] User without download permission attempted to download file "${libraryFile.metadata.path}"`, req.user) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`) | ||||
| 
 | ||||
|     if (global.XAccel) { | ||||
|       Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`) | ||||
|       return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send() | ||||
|     } | ||||
| 
 | ||||
|     // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
 | ||||
|     const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path)) | ||||
|     if (audioMimeType) { | ||||
|       res.setHeader('Content-Type', audioMimeType) | ||||
|     } | ||||
| 
 | ||||
|     res.download(libraryFile.metadata.path, libraryFile.metadata.filename) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * GET api/items/:id/ebook | ||||
|    *  | ||||
|    * @param {express.Request} req | ||||
|    * @param {express.Response} res  | ||||
|    */ | ||||
|   async getEBookFile(req, res) { | ||||
|     const ebookFile = req.libraryItem.media.ebookFile | ||||
|     if (!ebookFile) { | ||||
| @ -560,18 +623,33 @@ class LibraryItemController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     const ebookFilePath = ebookFile.metadata.path | ||||
| 
 | ||||
|     if (global.XAccel) { | ||||
|       Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`) | ||||
|       return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send() | ||||
|     } | ||||
| 
 | ||||
|     res.sendFile(ebookFilePath) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     const item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!item || !item.media) return res.sendStatus(404) | ||||
|     req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!req.libraryItem?.media) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check user can access this library item
 | ||||
|     if (!req.user.checkCanAccessLibraryItem(item)) { | ||||
|     if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     // For library file routes, get the library file
 | ||||
|     if (req.params.fileid) { | ||||
|       req.libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid) | ||||
|       if (!req.libraryFile) { | ||||
|         Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`) | ||||
|         return res.sendStatus(404) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (req.path.includes('/play')) { | ||||
|       // allow POST requests using /play and /play/:episodeId
 | ||||
|     } else if (req.method == 'DELETE' && !req.user.canDelete) { | ||||
| @ -582,7 +660,6 @@ class LibraryItemController { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     req.libraryItem = item | ||||
|     next() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -31,6 +31,7 @@ class AudioTrack { | ||||
|     this.startOffset = startOffset | ||||
|     this.duration = audioFile.duration | ||||
|     this.title = audioFile.metadata.filename || '' | ||||
|     // TODO: Switch to /api/items/:id/file/:fileid
 | ||||
|     this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(audioFile.metadata.relPath)) | ||||
|     this.mimeType = audioFile.mimeType | ||||
|     this.codec = audioFile.codec || null | ||||
|  | ||||
| @ -28,6 +28,7 @@ class VideoTrack { | ||||
|     this.index = videoFile.index | ||||
|     this.duration = videoFile.duration | ||||
|     this.title = videoFile.metadata.filename || '' | ||||
|     // TODO: Switch to /api/items/:id/file/:fileid
 | ||||
|     this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath)) | ||||
|     this.mimeType = videoFile.mimeType | ||||
|     this.codec = videoFile.codec | ||||
|  | ||||
| @ -120,7 +120,9 @@ class ApiRouter { | ||||
|     this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this)) | ||||
|     this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this)) | ||||
|     this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this)) | ||||
|     this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this)) | ||||
|     this.router.get('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getLibraryFile.bind(this)) | ||||
|     this.router.delete('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this)) | ||||
|     this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this)) | ||||
|     this.router.get('/items/:id/ebook', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this)) | ||||
| 
 | ||||
|     //
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ const Path = require('path') | ||||
| const Logger = require('../Logger') | ||||
| const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') | ||||
| 
 | ||||
| // TODO: Deprecated as of 2.2.21 edge
 | ||||
| class StaticRouter { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user