Skip to content

Commit

Permalink
Merge pull request #3580 from mikiher/cover-image-performance
Browse files Browse the repository at this point in the history
Improve cover image performance
  • Loading branch information
advplyr authored Nov 2, 2024
2 parents 633ff81 + 7a49681 commit 654b1d6
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 45 deletions.
4 changes: 2 additions & 2 deletions client/store/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ 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) =>
(libraryItemId, timestamp = null, raw = false) => {
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
Expand Down
20 changes: 20 additions & 0 deletions server/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

/**
Expand Down
14 changes: 7 additions & 7 deletions server/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -268,17 +268,17 @@ 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))

// 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}`)
Expand All @@ -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',
Expand All @@ -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) {
Expand Down
41 changes: 11 additions & 30 deletions server/controllers/LibraryItemController.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,52 +342,33 @@ 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)
}
if (req.query.ts) res.set('Cache-Control', 'private, max-age=86400')

// Check if library item media has a cover path
if (!libraryItem.media.coverPath || !(await fs.pathExists(libraryItem.media.coverPath))) {
return res.sendStatus(404)
const libraryItemId = req.params.id
if (!libraryItemId) {
return res.sendStatus(400)
}

if (req.query.ts) res.set('Cache-Control', 'private, max-age=86400')

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 = {
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
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)
}

/**
Expand Down
19 changes: 13 additions & 6 deletions server/managers/CacheManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions server/models/LibraryItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 654b1d6

Please sign in to comment.