diff --git a/client/components/covers/AuthorImage.vue b/client/components/covers/AuthorImage.vue index 01926363..e320e552 100644 --- a/client/components/covers/AuthorImage.vue +++ b/client/components/covers/AuthorImage.vue @@ -56,7 +56,7 @@ export default { }, imgSrc() { 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: { diff --git a/client/players/LocalAudioPlayer.js b/client/players/LocalAudioPlayer.js index eb1484bb..7fc17e7a 100644 --- a/client/players/LocalAudioPlayer.js +++ b/client/players/LocalAudioPlayer.js @@ -147,7 +147,7 @@ export default class LocalAudioPlayer extends EventEmitter { timeoutRetry: { maxNumRetry: 4, retryDelayMs: 0, - maxRetryDelayMs: 0, + maxRetryDelayMs: 0 }, errorRetry: { maxNumRetry: 8, @@ -160,7 +160,7 @@ export default class LocalAudioPlayer extends EventEmitter { } return retry } - }, + } } } } @@ -194,7 +194,7 @@ export default class LocalAudioPlayer extends EventEmitter { setDirectPlay() { // 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.loadCurrentTrack() @@ -270,7 +270,7 @@ export default class LocalAudioPlayer extends EventEmitter { // Seeking Direct play if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) { // 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) { this.startTime = time this.currentTrackIndex = trackIndex @@ -293,7 +293,6 @@ export default class LocalAudioPlayer extends EventEmitter { this.player.volume = volume } - // Utils isValidDuration(duration) { if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { @@ -338,4 +337,4 @@ export default class LocalAudioPlayer extends EventEmitter { var last = bufferedRanges[bufferedRanges.length - 1] return last.end } -} \ No newline at end of file +} diff --git a/client/plugins/constants.js b/client/plugins/constants.js index d89fbbbd..90c40b8c 100644 --- a/client/plugins/constants.js +++ b/client/plugins/constants.js @@ -1,6 +1,6 @@ const SupportedFileTypes = { 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'], info: ['nfo'], text: ['txt'], @@ -81,11 +81,9 @@ const Hotkeys = { } } -export { - Constants -} +export { Constants } export default ({ app }, inject) => { inject('constants', Constants) inject('keynames', KeyNames) inject('hotkeys', Hotkeys) -} \ No newline at end of file +} diff --git a/client/store/globals.js b/client/store/globals.js index c0e7d788..65878fb4 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -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 diff --git a/server/Auth.js b/server/Auth.js index 60af2a1e..5b2d8bcd 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -18,6 +18,26 @@ class Auth { constructor() { // Map of openId sessions indexed by oauth2 state-variable 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) + } } /** diff --git a/server/Server.js b/server/Server.js index d8265237..58a2079e 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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) { diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 54a64185..45bbdf84 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -381,16 +381,23 @@ class AuthorController { */ async getImage(req, res) { const { - query: { width, height, format, raw }, - author + query: { width, height, format, raw } } = req - if (!author.imagePath || !(await fs.pathExists(author.imagePath))) { - Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`) - return res.sendStatus(404) - } + 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))) { + Logger.warn(`[AuthorController] Author "${author.name}" has invalid imagePath: ${author.imagePath}`) + return res.sendStatus(404) + } + return res.sendFile(author.imagePath) } @@ -399,7 +406,7 @@ class AuthorController { height: height ? parseInt(height) : null, width: width ? parseInt(width) : null } - return CacheManager.handleAuthorCache(res, author, options) + return CacheManager.handleAuthorCache(res, authorId, options) } /** diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index a51a6e06..64069ac5 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -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) } /** diff --git a/server/managers/CacheManager.js b/server/managers/CacheManager.js index b4d2f270..f0375691 100644 --- a/server/managers/CacheManager.js +++ b/server/managers/CacheManager.js @@ -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) { @@ -127,22 +134,22 @@ class CacheManager { /** * * @param {import('express').Response} res - * @param {import('../models/Author')} author + * @param {String} authorId * @param {{ format?: string, width?: number, height?: number }} options * @returns */ - async handleAuthorCache(res, author, options = {}) { + async handleAuthorCache(res, authorId, options = {}) { const format = options.format || 'webp' const width = options.width || 400 const height = options.height || null 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 - if (await fs.pathExists(path)) { - const r = fs.createReadStream(path) + if (await fs.pathExists(cachePath)) { + const r = fs.createReadStream(cachePath) const ps = new stream.PassThrough() stream.pipeline(r, ps, (err) => { if (err) { @@ -153,7 +160,12 @@ class CacheManager { 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) var readStream = fs.createReadStream(writtenFile) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index dd07747a..17c3b125 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -863,6 +863,33 @@ class LibraryItem extends Model { return this.getOldLibraryItem(libraryItem) } + /** + * + * @param {string} libraryItemId + * @returns {Promise} + */ + 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 diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index f81bc26d..c9399d79 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -216,7 +216,7 @@ class ApiRouter { 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.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.delete('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.deleteImage.bind(this)) diff --git a/server/utils/constants.js b/server/utils/constants.js index cbfe65f2..dd52e2e1 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -49,5 +49,7 @@ module.exports.AudioMimeType = { WEBMA: 'audio/webm', MKA: 'audio/x-matroska', AWB: 'audio/amr-wb', - CAF: 'audio/x-caf' + CAF: 'audio/x-caf', + MPEG: 'audio/mpeg', + MPG: 'audio/mpeg' } diff --git a/server/utils/globals.js b/server/utils/globals.js index 877cf07a..5a5bd951 100644 --- a/server/utils/globals.js +++ b/server/utils/globals.js @@ -1,6 +1,6 @@ const globals = { 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'], TextFileTypes: ['txt', 'nfo'], MetadataFileTypes: ['opf', 'abs', 'xml', 'json'] diff --git a/server/utils/parsers/parseNameString.js b/server/utils/parsers/parseNameString.js index 741beb09..4b16b496 100644 --- a/server/utils/parsers/parseNameString.js +++ b/server/utils/parsers/parseNameString.js @@ -52,6 +52,13 @@ module.exports.parse = (nameString) => { } 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 = [] // 1 name FIRST LAST