mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #3580 from mikiher/cover-image-performance
Improve cover image performance
This commit is contained in:
		
						commit
						654b1d6b34
					
				| @ -98,7 +98,7 @@ export const getters = { | ||||
|       const userToken = rootGetters['user/getToken'] | ||||
|       const lastUpdate = libraryItem.updatedAt || Date.now() | ||||
|       const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
 | ||||
|       return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}` | ||||
|       return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?ts=${lastUpdate}${raw ? '&raw=1' : ''}` | ||||
|     }, | ||||
|   getLibraryItemCoverSrcById: | ||||
|     (state, getters, rootState, rootGetters) => | ||||
| @ -106,7 +106,7 @@ export const getters = { | ||||
|       const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` | ||||
|       if (!libraryItemId) return placeholder | ||||
|       const userToken = rootGetters['user/getToken'] | ||||
|       return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}` | ||||
|       return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}` | ||||
|     }, | ||||
|   getIsBatchSelectingMediaItems: (state) => { | ||||
|     return state.selectedMediaItems.length | ||||
|  | ||||
| @ -18,6 +18,26 @@ class Auth { | ||||
|   constructor() { | ||||
|     // Map of openId sessions indexed by oauth2 state-variable
 | ||||
|     this.openIdAuthSession = new Map() | ||||
|     this.ignorePattern = /\/api\/items\/[^/]+\/cover/ | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if the request should not be authenticated. | ||||
|    * @param {Request} req | ||||
|    * @returns {boolean} | ||||
|    * @private | ||||
|    */ | ||||
|   authNotNeeded(req) { | ||||
|     return req.method === 'GET' && this.ignorePattern.test(req.originalUrl) | ||||
|   } | ||||
| 
 | ||||
|   ifAuthNeeded(middleware) { | ||||
|     return (req, res, next) => { | ||||
|       if (this.authNotNeeded(req)) { | ||||
|         return next() | ||||
|       } | ||||
|       middleware(req, res, next) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -238,7 +238,7 @@ class Server { | ||||
|     // init passport.js
 | ||||
|     app.use(passport.initialize()) | ||||
|     // register passport in express-session
 | ||||
|     app.use(passport.session()) | ||||
|     app.use(this.auth.ifAuthNeeded(passport.session())) | ||||
|     // config passport.js
 | ||||
|     await this.auth.initPassportJs() | ||||
| 
 | ||||
| @ -268,6 +268,10 @@ class Server { | ||||
|     router.use(express.urlencoded({ extended: true, limit: '5mb' })) | ||||
|     router.use(express.json({ limit: '5mb' })) | ||||
| 
 | ||||
|     router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router) | ||||
|     router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) | ||||
|     router.use('/public', this.publicRouter.router) | ||||
| 
 | ||||
|     // Static path to generated nuxt
 | ||||
|     const distPath = Path.join(global.appRoot, '/client/dist') | ||||
|     router.use(express.static(distPath)) | ||||
| @ -275,10 +279,6 @@ class Server { | ||||
|     // Static folder
 | ||||
|     router.use(express.static(Path.join(global.appRoot, 'static'))) | ||||
| 
 | ||||
|     router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) | ||||
|     router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) | ||||
|     router.use('/public', this.publicRouter.router) | ||||
| 
 | ||||
|     // RSS Feed temp route
 | ||||
|     router.get('/feed/:slug', (req, res) => { | ||||
|       Logger.info(`[Server] Requesting rss feed ${req.params.slug}`) | ||||
| @ -296,7 +296,7 @@ class Server { | ||||
|     await this.auth.initAuthRoutes(router) | ||||
| 
 | ||||
|     // Client dynamic routes
 | ||||
|     const dyanimicRoutes = [ | ||||
|     const dynamicRoutes = [ | ||||
|       '/item/:id', | ||||
|       '/author/:id', | ||||
|       '/audiobook/:id/chapters', | ||||
| @ -319,7 +319,7 @@ class Server { | ||||
|       '/playlist/:id', | ||||
|       '/share/:slug' | ||||
|     ] | ||||
|     dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) | ||||
|     dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html')))) | ||||
| 
 | ||||
|     router.post('/init', (req, res) => { | ||||
|       if (Database.hasRootUser) { | ||||
|  | ||||
| @ -342,44 +342,25 @@ class LibraryItemController { | ||||
|       query: { width, height, format, raw } | ||||
|     } = req | ||||
| 
 | ||||
|     const libraryItem = await Database.libraryItemModel.findByPk(req.params.id, { | ||||
|       attributes: ['id', 'mediaType', 'mediaId', 'libraryId'], | ||||
|       include: [ | ||||
|         { | ||||
|           model: Database.bookModel, | ||||
|           attributes: ['id', 'coverPath', 'tags', 'explicit'] | ||||
|         }, | ||||
|         { | ||||
|           model: Database.podcastModel, | ||||
|           attributes: ['id', 'coverPath', 'tags', 'explicit'] | ||||
|         } | ||||
|       ] | ||||
|     }) | ||||
|     if (!libraryItem) { | ||||
|       Logger.warn(`[LibraryItemController] getCover: Library item "${req.params.id}" does not exist`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     // Check if user can access this library item
 | ||||
|     if (!req.user.checkCanAccessLibraryItem(libraryItem)) { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     // Check if library item media has a cover path
 | ||||
|     if (!libraryItem.media.coverPath || !(await fs.pathExists(libraryItem.media.coverPath))) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     if (req.query.ts) res.set('Cache-Control', 'private, max-age=86400') | ||||
| 
 | ||||
|     const libraryItemId = req.params.id | ||||
|     if (!libraryItemId) { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| 
 | ||||
|     if (raw) { | ||||
|       const coverPath = await Database.libraryItemModel.getCoverPath(libraryItemId) | ||||
|       if (!coverPath || !(await fs.pathExists(coverPath))) { | ||||
|         return res.sendStatus(404) | ||||
|       } | ||||
|       // any value
 | ||||
|       if (global.XAccel) { | ||||
|         const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath) | ||||
|         const encodedURI = encodeUriPath(global.XAccel + coverPath) | ||||
|         Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) | ||||
|         return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() | ||||
|       } | ||||
|       return res.sendFile(libraryItem.media.coverPath) | ||||
|       return res.sendFile(coverPath) | ||||
|     } | ||||
| 
 | ||||
|     const options = { | ||||
| @ -387,7 +368,7 @@ class LibraryItemController { | ||||
|       height: height ? parseInt(height) : null, | ||||
|       width: width ? parseInt(width) : null | ||||
|     } | ||||
|     return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options) | ||||
|     return CacheManager.handleCoverCache(res, libraryItemId, options) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -4,6 +4,7 @@ const stream = require('stream') | ||||
| const Logger = require('../Logger') | ||||
| const { resizeImage } = require('../utils/ffmpegHelpers') | ||||
| const { encodeUriPath } = require('../utils/fileUtils') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class CacheManager { | ||||
|   constructor() { | ||||
| @ -29,24 +30,24 @@ class CacheManager { | ||||
|     await fs.ensureDir(this.ItemCachePath) | ||||
|   } | ||||
| 
 | ||||
|   async handleCoverCache(res, libraryItemId, coverPath, options = {}) { | ||||
|   async handleCoverCache(res, libraryItemId, options = {}) { | ||||
|     const format = options.format || 'webp' | ||||
|     const width = options.width || 400 | ||||
|     const height = options.height || null | ||||
| 
 | ||||
|     res.type(`image/${format}`) | ||||
| 
 | ||||
|     const path = Path.join(this.CoverCachePath, `${libraryItemId}_${width}${height ? `x${height}` : ''}`) + '.' + format | ||||
|     const cachePath = Path.join(this.CoverCachePath, `${libraryItemId}_${width}${height ? `x${height}` : ''}`) + '.' + format | ||||
| 
 | ||||
|     // Cache exists
 | ||||
|     if (await fs.pathExists(path)) { | ||||
|     if (await fs.pathExists(cachePath)) { | ||||
|       if (global.XAccel) { | ||||
|         const encodedURI = encodeUriPath(global.XAccel + path) | ||||
|         const encodedURI = encodeUriPath(global.XAccel + cachePath) | ||||
|         Logger.debug(`Use X-Accel to serve static file ${encodedURI}`) | ||||
|         return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send() | ||||
|       } | ||||
| 
 | ||||
|       const r = fs.createReadStream(path) | ||||
|       const r = fs.createReadStream(cachePath) | ||||
|       const ps = new stream.PassThrough() | ||||
|       stream.pipeline(r, ps, (err) => { | ||||
|         if (err) { | ||||
| @ -57,7 +58,13 @@ class CacheManager { | ||||
|       return ps.pipe(res) | ||||
|     } | ||||
| 
 | ||||
|     const writtenFile = await resizeImage(coverPath, path, width, height) | ||||
|     // Cached cover does not exist, generate it
 | ||||
|     const coverPath = await Database.libraryItemModel.getCoverPath(libraryItemId) | ||||
|     if (!coverPath || !(await fs.pathExists(coverPath))) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     const writtenFile = await resizeImage(coverPath, cachePath, width, height) | ||||
|     if (!writtenFile) return res.sendStatus(500) | ||||
| 
 | ||||
|     if (global.XAccel) { | ||||
|  | ||||
| @ -863,6 +863,33 @@ class LibraryItem extends Model { | ||||
|     return this.getOldLibraryItem(libraryItem) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {string} libraryItemId | ||||
|    * @returns {Promise<string>} | ||||
|    */ | ||||
|   static async getCoverPath(libraryItemId) { | ||||
|     const libraryItem = await this.findByPk(libraryItemId, { | ||||
|       attributes: ['id', 'mediaType', 'mediaId', 'libraryId'], | ||||
|       include: [ | ||||
|         { | ||||
|           model: this.sequelize.models.book, | ||||
|           attributes: ['id', 'coverPath'] | ||||
|         }, | ||||
|         { | ||||
|           model: this.sequelize.models.podcast, | ||||
|           attributes: ['id', 'coverPath'] | ||||
|         } | ||||
|       ] | ||||
|     }) | ||||
|     if (!libraryItem) { | ||||
|       Logger.warn(`[LibraryItem] getCoverPath: Library item "${libraryItemId}" does not exist`) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     return libraryItem.media.coverPath | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('sequelize').FindOptions} options | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user