mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'advplyr:master' into multi-select-keyboard-navigation
This commit is contained in:
		
						commit
						588def6d33
					
				@ -56,7 +56,7 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    imgSrc() {
 | 
					    imgSrc() {
 | 
				
			||||||
      if (!this.imagePath) return null
 | 
					      if (!this.imagePath) return null
 | 
				
			||||||
      return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
 | 
					      return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
				
			|||||||
@ -147,7 +147,7 @@ export default class LocalAudioPlayer extends EventEmitter {
 | 
				
			|||||||
          timeoutRetry: {
 | 
					          timeoutRetry: {
 | 
				
			||||||
            maxNumRetry: 4,
 | 
					            maxNumRetry: 4,
 | 
				
			||||||
            retryDelayMs: 0,
 | 
					            retryDelayMs: 0,
 | 
				
			||||||
            maxRetryDelayMs: 0,
 | 
					            maxRetryDelayMs: 0
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          errorRetry: {
 | 
					          errorRetry: {
 | 
				
			||||||
            maxNumRetry: 8,
 | 
					            maxNumRetry: 8,
 | 
				
			||||||
@ -160,7 +160,7 @@ export default class LocalAudioPlayer extends EventEmitter {
 | 
				
			|||||||
              }
 | 
					              }
 | 
				
			||||||
              return retry
 | 
					              return retry
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -194,7 +194,7 @@ export default class LocalAudioPlayer extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  setDirectPlay() {
 | 
					  setDirectPlay() {
 | 
				
			||||||
    // Set initial track and track time offset
 | 
					    // Set initial track and track time offset
 | 
				
			||||||
    var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration))
 | 
					    var trackIndex = this.audioTracks.findIndex((t) => this.startTime >= t.startOffset && this.startTime < t.startOffset + t.duration)
 | 
				
			||||||
    this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
 | 
					    this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.loadCurrentTrack()
 | 
					    this.loadCurrentTrack()
 | 
				
			||||||
@ -270,7 +270,7 @@ export default class LocalAudioPlayer extends EventEmitter {
 | 
				
			|||||||
      // Seeking Direct play
 | 
					      // Seeking Direct play
 | 
				
			||||||
      if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
 | 
					      if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
 | 
				
			||||||
        // Change Track
 | 
					        // Change Track
 | 
				
			||||||
        var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration))
 | 
					        var trackIndex = this.audioTracks.findIndex((t) => time >= t.startOffset && time < t.startOffset + t.duration)
 | 
				
			||||||
        if (trackIndex >= 0) {
 | 
					        if (trackIndex >= 0) {
 | 
				
			||||||
          this.startTime = time
 | 
					          this.startTime = time
 | 
				
			||||||
          this.currentTrackIndex = trackIndex
 | 
					          this.currentTrackIndex = trackIndex
 | 
				
			||||||
@ -293,7 +293,6 @@ export default class LocalAudioPlayer extends EventEmitter {
 | 
				
			|||||||
    this.player.volume = volume
 | 
					    this.player.volume = volume
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Utils
 | 
					  // Utils
 | 
				
			||||||
  isValidDuration(duration) {
 | 
					  isValidDuration(duration) {
 | 
				
			||||||
    if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
 | 
					    if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
const SupportedFileTypes = {
 | 
					const SupportedFileTypes = {
 | 
				
			||||||
  image: ['png', 'jpg', 'jpeg', 'webp'],
 | 
					  image: ['png', 'jpg', 'jpeg', 'webp'],
 | 
				
			||||||
  audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
 | 
					  audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
 | 
				
			||||||
  ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
 | 
					  ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
 | 
				
			||||||
  info: ['nfo'],
 | 
					  info: ['nfo'],
 | 
				
			||||||
  text: ['txt'],
 | 
					  text: ['txt'],
 | 
				
			||||||
@ -81,9 +81,7 @@ const Hotkeys = {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export { Constants }
 | 
				
			||||||
  Constants
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export default ({ app }, inject) => {
 | 
					export default ({ app }, inject) => {
 | 
				
			||||||
  inject('constants', Constants)
 | 
					  inject('constants', Constants)
 | 
				
			||||||
  inject('keynames', KeyNames)
 | 
					  inject('keynames', KeyNames)
 | 
				
			||||||
 | 
				
			|||||||
@ -98,7 +98,7 @@ export const getters = {
 | 
				
			|||||||
      const userToken = rootGetters['user/getToken']
 | 
					      const userToken = rootGetters['user/getToken']
 | 
				
			||||||
      const lastUpdate = libraryItem.updatedAt || Date.now()
 | 
					      const lastUpdate = libraryItem.updatedAt || Date.now()
 | 
				
			||||||
      const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
 | 
					      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:
 | 
					  getLibraryItemCoverSrcById:
 | 
				
			||||||
    (state, getters, rootState, rootGetters) =>
 | 
					    (state, getters, rootState, rootGetters) =>
 | 
				
			||||||
@ -106,7 +106,7 @@ export const getters = {
 | 
				
			|||||||
      const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
 | 
					      const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
 | 
				
			||||||
      if (!libraryItemId) return placeholder
 | 
					      if (!libraryItemId) return placeholder
 | 
				
			||||||
      const userToken = rootGetters['user/getToken']
 | 
					      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) => {
 | 
					  getIsBatchSelectingMediaItems: (state) => {
 | 
				
			||||||
    return state.selectedMediaItems.length
 | 
					    return state.selectedMediaItems.length
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,26 @@ class Auth {
 | 
				
			|||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    // Map of openId sessions indexed by oauth2 state-variable
 | 
					    // Map of openId sessions indexed by oauth2 state-variable
 | 
				
			||||||
    this.openIdAuthSession = new Map()
 | 
					    this.openIdAuthSession = new Map()
 | 
				
			||||||
 | 
					    this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Checks if the request should not be authenticated.
 | 
				
			||||||
 | 
					   * @param {Request} req
 | 
				
			||||||
 | 
					   * @returns {boolean}
 | 
				
			||||||
 | 
					   * @private
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  authNotNeeded(req) {
 | 
				
			||||||
 | 
					    return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.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
 | 
					    // init passport.js
 | 
				
			||||||
    app.use(passport.initialize())
 | 
					    app.use(passport.initialize())
 | 
				
			||||||
    // register passport in express-session
 | 
					    // register passport in express-session
 | 
				
			||||||
    app.use(passport.session())
 | 
					    app.use(this.auth.ifAuthNeeded(passport.session()))
 | 
				
			||||||
    // config passport.js
 | 
					    // config passport.js
 | 
				
			||||||
    await this.auth.initPassportJs()
 | 
					    await this.auth.initPassportJs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -268,6 +268,10 @@ class Server {
 | 
				
			|||||||
    router.use(express.urlencoded({ extended: true, limit: '5mb' }))
 | 
					    router.use(express.urlencoded({ extended: true, limit: '5mb' }))
 | 
				
			||||||
    router.use(express.json({ 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
 | 
					    // Static path to generated nuxt
 | 
				
			||||||
    const distPath = Path.join(global.appRoot, '/client/dist')
 | 
					    const distPath = Path.join(global.appRoot, '/client/dist')
 | 
				
			||||||
    router.use(express.static(distPath))
 | 
					    router.use(express.static(distPath))
 | 
				
			||||||
@ -275,10 +279,6 @@ class Server {
 | 
				
			|||||||
    // Static folder
 | 
					    // Static folder
 | 
				
			||||||
    router.use(express.static(Path.join(global.appRoot, 'static')))
 | 
					    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
 | 
					    // RSS Feed temp route
 | 
				
			||||||
    router.get('/feed/:slug', (req, res) => {
 | 
					    router.get('/feed/:slug', (req, res) => {
 | 
				
			||||||
      Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
 | 
					      Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
 | 
				
			||||||
@ -296,7 +296,7 @@ class Server {
 | 
				
			|||||||
    await this.auth.initAuthRoutes(router)
 | 
					    await this.auth.initAuthRoutes(router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Client dynamic routes
 | 
					    // Client dynamic routes
 | 
				
			||||||
    const dyanimicRoutes = [
 | 
					    const dynamicRoutes = [
 | 
				
			||||||
      '/item/:id',
 | 
					      '/item/:id',
 | 
				
			||||||
      '/author/:id',
 | 
					      '/author/:id',
 | 
				
			||||||
      '/audiobook/:id/chapters',
 | 
					      '/audiobook/:id/chapters',
 | 
				
			||||||
@ -319,7 +319,7 @@ class Server {
 | 
				
			|||||||
      '/playlist/:id',
 | 
					      '/playlist/:id',
 | 
				
			||||||
      '/share/:slug'
 | 
					      '/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) => {
 | 
					    router.post('/init', (req, res) => {
 | 
				
			||||||
      if (Database.hasRootUser) {
 | 
					      if (Database.hasRootUser) {
 | 
				
			||||||
 | 
				
			|||||||
@ -381,16 +381,23 @@ class AuthorController {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  async getImage(req, res) {
 | 
					  async getImage(req, res) {
 | 
				
			||||||
    const {
 | 
					    const {
 | 
				
			||||||
      query: { width, height, format, raw },
 | 
					      query: { width, height, format, raw }
 | 
				
			||||||
      author
 | 
					 | 
				
			||||||
    } = req
 | 
					    } = req
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const authorId = req.params.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (raw) {
 | 
				
			||||||
 | 
					      const author = await Database.authorModel.findByPk(authorId)
 | 
				
			||||||
 | 
					      if (!author) {
 | 
				
			||||||
 | 
					        Logger.warn(`[AuthorController] Author "${authorId}" not found`)
 | 
				
			||||||
 | 
					        return res.sendStatus(404)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
 | 
					      if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
 | 
				
			||||||
        Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`)
 | 
					        Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`)
 | 
				
			||||||
        return res.sendStatus(404)
 | 
					        return res.sendStatus(404)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (raw) {
 | 
					 | 
				
			||||||
      return res.sendFile(author.imagePath)
 | 
					      return res.sendFile(author.imagePath)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -399,7 +406,7 @@ class AuthorController {
 | 
				
			|||||||
      height: height ? parseInt(height) : null,
 | 
					      height: height ? parseInt(height) : null,
 | 
				
			||||||
      width: width ? parseInt(width) : null
 | 
					      width: width ? parseInt(width) : null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return CacheManager.handleAuthorCache(res, author, options)
 | 
					    return CacheManager.handleAuthorCache(res, authorId, options)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
 | 
				
			|||||||
@ -342,44 +342,25 @@ class LibraryItemController {
 | 
				
			|||||||
      query: { width, height, format, raw }
 | 
					      query: { width, height, format, raw }
 | 
				
			||||||
    } = req
 | 
					    } = 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')
 | 
					    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) {
 | 
					    if (raw) {
 | 
				
			||||||
 | 
					      const coverPath = await Database.libraryItemModel.getCoverPath(libraryItemId)
 | 
				
			||||||
 | 
					      if (!coverPath || !(await fs.pathExists(coverPath))) {
 | 
				
			||||||
 | 
					        return res.sendStatus(404)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      // any value
 | 
					      // any value
 | 
				
			||||||
      if (global.XAccel) {
 | 
					      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}`)
 | 
					        Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
 | 
				
			||||||
        return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
 | 
					        return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return res.sendFile(libraryItem.media.coverPath)
 | 
					      return res.sendFile(coverPath)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = {
 | 
					    const options = {
 | 
				
			||||||
@ -387,7 +368,7 @@ class LibraryItemController {
 | 
				
			|||||||
      height: height ? parseInt(height) : null,
 | 
					      height: height ? parseInt(height) : null,
 | 
				
			||||||
      width: width ? parseInt(width) : 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 Logger = require('../Logger')
 | 
				
			||||||
const { resizeImage } = require('../utils/ffmpegHelpers')
 | 
					const { resizeImage } = require('../utils/ffmpegHelpers')
 | 
				
			||||||
const { encodeUriPath } = require('../utils/fileUtils')
 | 
					const { encodeUriPath } = require('../utils/fileUtils')
 | 
				
			||||||
 | 
					const Database = require('../Database')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CacheManager {
 | 
					class CacheManager {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
@ -29,24 +30,24 @@ class CacheManager {
 | 
				
			|||||||
    await fs.ensureDir(this.ItemCachePath)
 | 
					    await fs.ensureDir(this.ItemCachePath)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleCoverCache(res, libraryItemId, coverPath, options = {}) {
 | 
					  async handleCoverCache(res, libraryItemId, options = {}) {
 | 
				
			||||||
    const format = options.format || 'webp'
 | 
					    const format = options.format || 'webp'
 | 
				
			||||||
    const width = options.width || 400
 | 
					    const width = options.width || 400
 | 
				
			||||||
    const height = options.height || null
 | 
					    const height = options.height || null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.type(`image/${format}`)
 | 
					    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
 | 
					    // Cache exists
 | 
				
			||||||
    if (await fs.pathExists(path)) {
 | 
					    if (await fs.pathExists(cachePath)) {
 | 
				
			||||||
      if (global.XAccel) {
 | 
					      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}`)
 | 
					        Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
 | 
				
			||||||
        return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
 | 
					        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()
 | 
					      const ps = new stream.PassThrough()
 | 
				
			||||||
      stream.pipeline(r, ps, (err) => {
 | 
					      stream.pipeline(r, ps, (err) => {
 | 
				
			||||||
        if (err) {
 | 
					        if (err) {
 | 
				
			||||||
@ -57,7 +58,13 @@ class CacheManager {
 | 
				
			|||||||
      return ps.pipe(res)
 | 
					      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 (!writtenFile) return res.sendStatus(500)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (global.XAccel) {
 | 
					    if (global.XAccel) {
 | 
				
			||||||
@ -127,22 +134,22 @@ class CacheManager {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('express').Response} res
 | 
					   * @param {import('express').Response} res
 | 
				
			||||||
   * @param {import('../models/Author')} author
 | 
					   * @param {String} authorId
 | 
				
			||||||
   * @param {{ format?: string, width?: number, height?: number }} options
 | 
					   * @param {{ format?: string, width?: number, height?: number }} options
 | 
				
			||||||
   * @returns
 | 
					   * @returns
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async handleAuthorCache(res, author, options = {}) {
 | 
					  async handleAuthorCache(res, authorId, options = {}) {
 | 
				
			||||||
    const format = options.format || 'webp'
 | 
					    const format = options.format || 'webp'
 | 
				
			||||||
    const width = options.width || 400
 | 
					    const width = options.width || 400
 | 
				
			||||||
    const height = options.height || null
 | 
					    const height = options.height || null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.type(`image/${format}`)
 | 
					    res.type(`image/${format}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var path = Path.join(this.ImageCachePath, `${author.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
 | 
					    var cachePath = Path.join(this.ImageCachePath, `${authorId}_${width}${height ? `x${height}` : ''}`) + '.' + format
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Cache exists
 | 
					    // Cache exists
 | 
				
			||||||
    if (await fs.pathExists(path)) {
 | 
					    if (await fs.pathExists(cachePath)) {
 | 
				
			||||||
      const r = fs.createReadStream(path)
 | 
					      const r = fs.createReadStream(cachePath)
 | 
				
			||||||
      const ps = new stream.PassThrough()
 | 
					      const ps = new stream.PassThrough()
 | 
				
			||||||
      stream.pipeline(r, ps, (err) => {
 | 
					      stream.pipeline(r, ps, (err) => {
 | 
				
			||||||
        if (err) {
 | 
					        if (err) {
 | 
				
			||||||
@ -153,7 +160,12 @@ class CacheManager {
 | 
				
			|||||||
      return ps.pipe(res)
 | 
					      return ps.pipe(res)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let writtenFile = await resizeImage(author.imagePath, path, width, height)
 | 
					    const author = await Database.authorModel.findByPk(authorId)
 | 
				
			||||||
 | 
					    if (!author || !author.imagePath || !(await fs.pathExists(author.imagePath))) {
 | 
				
			||||||
 | 
					      return res.sendStatus(404)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let writtenFile = await resizeImage(author.imagePath, cachePath, width, height)
 | 
				
			||||||
    if (!writtenFile) return res.sendStatus(500)
 | 
					    if (!writtenFile) return res.sendStatus(500)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var readStream = fs.createReadStream(writtenFile)
 | 
					    var readStream = fs.createReadStream(writtenFile)
 | 
				
			||||||
 | 
				
			|||||||
@ -863,6 +863,33 @@ class LibraryItem extends Model {
 | 
				
			|||||||
    return this.getOldLibraryItem(libraryItem)
 | 
					    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
 | 
					   * @param {import('sequelize').FindOptions} options
 | 
				
			||||||
 | 
				
			|||||||
@ -216,7 +216,7 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.patch('/authors/:id', AuthorController.middleware.bind(this), AuthorController.update.bind(this))
 | 
					    this.router.patch('/authors/:id', AuthorController.middleware.bind(this), AuthorController.update.bind(this))
 | 
				
			||||||
    this.router.delete('/authors/:id', AuthorController.middleware.bind(this), AuthorController.delete.bind(this))
 | 
					    this.router.delete('/authors/:id', AuthorController.middleware.bind(this), AuthorController.delete.bind(this))
 | 
				
			||||||
    this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
 | 
					    this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
 | 
				
			||||||
    this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
 | 
					    this.router.get('/authors/:id/image', AuthorController.getImage.bind(this))
 | 
				
			||||||
    this.router.post('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.uploadImage.bind(this))
 | 
					    this.router.post('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.uploadImage.bind(this))
 | 
				
			||||||
    this.router.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this))
 | 
					    this.router.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -49,5 +49,7 @@ module.exports.AudioMimeType = {
 | 
				
			|||||||
  WEBMA: 'audio/webm',
 | 
					  WEBMA: 'audio/webm',
 | 
				
			||||||
  MKA: 'audio/x-matroska',
 | 
					  MKA: 'audio/x-matroska',
 | 
				
			||||||
  AWB: 'audio/amr-wb',
 | 
					  AWB: 'audio/amr-wb',
 | 
				
			||||||
  CAF: 'audio/x-caf'
 | 
					  CAF: 'audio/x-caf',
 | 
				
			||||||
 | 
					  MPEG: 'audio/mpeg',
 | 
				
			||||||
 | 
					  MPG: 'audio/mpeg'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
const globals = {
 | 
					const globals = {
 | 
				
			||||||
  SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
 | 
					  SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
 | 
				
			||||||
  SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
 | 
					  SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpg', 'mpeg'],
 | 
				
			||||||
  SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
 | 
					  SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
 | 
				
			||||||
  TextFileTypes: ['txt', 'nfo'],
 | 
					  TextFileTypes: ['txt', 'nfo'],
 | 
				
			||||||
  MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
 | 
					  MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,13 @@ module.exports.parse = (nameString) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
 | 
					  if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // If names are in Chinese,Japanese and Korean languages, return as is.
 | 
				
			||||||
 | 
					  if (/[\u4e00-\u9fff\u3040-\u30ff\u31f0-\u31ff]/.test(splitNames[0])) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      names: splitNames
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var names = []
 | 
					  var names = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 1 name FIRST LAST
 | 
					  // 1 name FIRST LAST
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user