diff --git a/client/components/modals/edit-tabs/Cover.vue b/client/components/modals/edit-tabs/Cover.vue index cae24a89..40d189fc 100644 --- a/client/components/modals/edit-tabs/Cover.vue +++ b/client/components/modals/edit-tabs/Cover.vue @@ -162,7 +162,11 @@ export default { }) .catch((error) => { console.error('Failed', error) - this.$toast.error('Oops, something went wrong...') + if (error.response && error.response.data) { + this.$toast.error(error.response.data) + } else { + this.$toast.error('Oops, something went wrong...') + } this.processingUpload = false }) }, @@ -204,20 +208,39 @@ export default { } this.isProcessing = true - const updatePayload = { - book: { - cover: cover + var success = false + + // Download cover from url and use + if (cover.startsWith('http:') || cover.startsWith('https:')) { + success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, { url: cover }).catch((error) => { + console.error('Failed to download cover from url', error) + if (error.response && error.response.data) { + this.$toast.error(error.response.data) + } + return false + }) + } else { + // Update local cover url + const updatePayload = { + book: { + cover: cover + } } + success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { + console.error('Failed to update', error) + if (error.response && error.response.data) { + this.$toast.error(error.response.data) + } + return false + }) } - var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { - console.error('Failed to update', error) - return false - }) - this.isProcessing = false - if (updatedAudiobook) { + if (success) { this.$toast.success('Update Successful') this.$emit('close') + } else { + this.imageUrl = this.book.cover || '' } + this.isProcessing = false }, getSearchQuery() { var searchQuery = `provider=openlibrary&title=${this.searchTitle}` diff --git a/client/components/ui/FileInput.vue b/client/components/ui/FileInput.vue index a85859b3..cbfd28a0 100644 --- a/client/components/ui/FileInput.vue +++ b/client/components/ui/FileInput.vue @@ -9,7 +9,7 @@ export default { data() { return { - inputAccept: 'image/*' + inputAccept: '.png, .jpg, .jpeg, .webp' } }, computed: {}, diff --git a/client/package.json b/client/package.json index e753a7ca..91940d35 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.3.2", + "version": "1.3.3", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue index 2c01a646..30afb57c 100644 --- a/client/pages/upload/index.vue +++ b/client/pages/upload/index.vue @@ -121,8 +121,8 @@ export default { author: null, series: null, acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac'], - acceptedImageFormats: ['image/*'], - inputAccept: 'image/*, .mp3, .m4b, .m4a, .flac', + acceptedImageFormats: ['.png', '.jpg', '.jpeg', '.webp'], + inputAccept: '.png, .jpg, .jpeg, .webp, .mp3, .m4b, .m4a, .flac', isDragOver: false, showUploader: true, validAudioFiles: [], diff --git a/package-lock.json b/package-lock.json index 3cfeeb3e..5cb90522 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.2.7", + "version": "1.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -573,6 +573,11 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz", "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==" }, + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -723,6 +728,14 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "image-type": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz", + "integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==", + "requires": { + "file-type": "^10.10.0" + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1032,6 +1045,16 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1047,6 +1070,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, "podcast": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/podcast/-/podcast-1.3.0.tgz", @@ -1124,6 +1152,15 @@ "unpipe": "1.0.0" } }, + "read-chunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.1.0.tgz", + "integrity": "sha512-ZdiZJXXoZYE08SzZvTipHhI+ZW0FpzxmFtLI3vIeMuRN9ySbIZ+SZawKogqJ7dxW9fJ/W73BNtxu4Zu/bZp+Ng==", + "requires": { + "pify": "^4.0.1", + "with-open-file": "^0.1.5" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -1424,6 +1461,16 @@ "isexe": "^2.0.0" } }, + "with-open-file": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz", + "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==", + "requires": { + "p-finally": "^1.0.0", + "p-try": "^2.1.0", + "pify": "^4.0.1" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 46696561..592e2f4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.3.2", + "version": "1.3.3", "description": "Self-hosted audiobook server for managing and playing audiobooks", "main": "index.js", "scripts": { @@ -32,12 +32,14 @@ "express-rate-limit": "^5.3.0", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^10.0.0", + "image-type": "^4.1.0", "ip": "^1.1.5", "jsonwebtoken": "^8.5.1", "libgen": "^2.1.0", "njodb": "^0.4.20", "node-dir": "^0.1.17", "podcast": "^1.3.0", + "read-chunk": "^3.1.0", "socket.io": "^4.1.3", "watcher": "^1.2.0" }, diff --git a/server/ApiController.js b/server/ApiController.js index 354032e4..7c16b49a 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -3,17 +3,17 @@ const Path = require('path') const fs = require('fs-extra') const Logger = require('./Logger') const User = require('./objects/User') -const { isObject, isAcceptableCoverMimeType } = require('./utils/index') -const { CoverDestination } = require('./utils/constants') +const { isObject } = require('./utils/index') class ApiController { - constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, emitter, clientEmitter) { + constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, emitter, clientEmitter) { this.db = db this.scanner = scanner this.auth = auth this.streamManager = streamManager this.rssFeeds = rssFeeds this.downloadManager = downloadManager + this.coverController = coverController this.emitter = emitter this.clientEmitter = clientEmitter this.MetadataPath = MetadataPath @@ -221,77 +221,36 @@ class ApiController { Logger.warn('User attempted to upload a cover without permission', req.user) return res.sendStatus(403) } - if (!req.files || !req.files.cover) { - return res.status(400).send('No files were uploaded') - } + var audiobookId = req.params.id var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) if (!audiobook) { return res.status(404).send('Audiobook not found') } - var coverFile = req.files.cover - var mimeType = coverFile.mimetype - var extname = Path.extname(coverFile.name.toLowerCase()) || '.jpg' - if (!isAcceptableCoverMimeType(mimeType)) { - return res.status(400).send('Invalid image file type: ' + mimeType) - } - - var coverDestination = this.db.serverSettings ? this.db.serverSettings.coverDestination : CoverDestination.METADATA - Logger.info(`[ApiController] Cover Upload destination ${coverDestination}`) - - var coverDirpath = audiobook.fullPath - var coverRelDirpath = Path.join('/local', audiobook.path) - if (coverDestination === CoverDestination.METADATA) { - coverDirpath = Path.join(this.MetadataPath, 'books', audiobookId) - coverRelDirpath = Path.join('/metadata', 'books', audiobookId) - Logger.debug(`[ApiController] storing in metadata | ${coverDirpath}`) - await fs.ensureDir(coverDirpath) + var result = null + if (req.body && req.body.url) { + Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`) + result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url) + } else if (req.files && req.files.cover) { + Logger.debug(`[ApiController] Handling uploaded cover`) + var coverFile = req.files.cover + result = await this.coverController.uploadCover(audiobook, coverFile) } else { - Logger.debug(`[ApiController] storing in audiobook | ${coverRelDirpath}`) + return res.status(400).send('Invalid request no file or url') } - var coverFilename = `cover${extname}` - var coverFullPath = Path.join(coverDirpath, coverFilename) - var coverPath = Path.join(coverRelDirpath, coverFilename) - - // If current cover is a metadata cover and does not match replacement, then remove it - var currentBookCover = audiobook.book.cover - if (currentBookCover && currentBookCover.startsWith(Path.sep + 'metadata')) { - Logger.debug(`Current Book Cover is metadata ${currentBookCover}`) - if (currentBookCover !== coverPath) { - Logger.info(`[ApiController] removing old metadata cover "${currentBookCover}"`) - var oldFullBookCoverPath = Path.join(this.MetadataPath, currentBookCover.replace(Path.sep + 'metadata', '')) - - // Metadata path may have changed, check if exists first - var exists = await fs.pathExists(oldFullBookCoverPath) - if (exists) { - try { - await fs.remove(oldFullBookCoverPath) - } catch (error) { - Logger.error(`[ApiController] Failed to remove old metadata book cover ${oldFullBookCoverPath}`) - } - } - } + if (result && result.error) { + return res.status(400).send(result.error) + } else if (!result || !result.cover) { + return res.status(500).send('Unknown error occurred') } - var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => { - Logger.error('Failed to move cover file', path, error) - return false - }) - - if (!success) { - return res.status(500).send('Failed to move cover into destination') - } - - Logger.info(`[ApiController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`) - - audiobook.updateBookCover(coverPath) await this.db.updateAudiobook(audiobook) this.emitter('audiobook_updated', audiobook.toJSONMinified()) res.json({ success: true, - cover: coverPath + cover: result.cover }) } diff --git a/server/CoverController.js b/server/CoverController.js new file mode 100644 index 00000000..fba0a791 --- /dev/null +++ b/server/CoverController.js @@ -0,0 +1,193 @@ +const fs = require('fs-extra') +const Path = require('path') +const axios = require('axios') +const Logger = require('./Logger') +const readChunk = require('read-chunk') +const imageType = require('image-type') + +const globals = require('./utils/globals') +const { CoverDestination } = require('./utils/constants') + + +class CoverController { + constructor(db, MetadataPath, AudiobookPath) { + this.db = db + this.MetadataPath = MetadataPath + this.BookMetadataPath = Path.join(this.MetadataPath, 'books') + this.AudiobookPath = AudiobookPath + } + + getCoverDirectory(audiobook) { + if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) { + return { + fullPath: audiobook.fullPath, + relPath: Path.join('/local', audiobook.path) + } + } else { + return { + fullPath: Path.join(this.BookMetadataPath, audiobook.id), + relPath: Path.join('/metadata', 'books', audiobook.id) + } + } + } + + getFilesInDirectory(dir) { + try { + return fs.readdir(dir) + } catch (error) { + Logger.error(`[CoverController] Failed to get files in dir ${dir}`, error) + return [] + } + } + + removeFile(filepath) { + try { + return fs.pathExists(filepath).then((exists) => { + if (!exists) Logger.warn(`[CoverController] Attempting to remove file that does not exist ${filepath}`) + return exists ? fs.unlink(filepath) : false + }) + } catch (error) { + Logger.error(`[CoverController] Failed to remove file "${filepath}"`, error) + return false + } + } + + // Remove covers in metadata/books/{ID} that dont have the same filename as the new cover + async checkBookMetadataCovers(dirpath, newCoverExt) { + var filesInDir = await this.getFilesInDirectory(dirpath) + + for (let i = 0; i < filesInDir.length; i++) { + var file = filesInDir[i] + var _extname = Path.extname(file) + var _filename = Path.basename(file, _extname) + if (_filename === 'cover' && _extname !== newCoverExt) { + var filepath = Path.join(dirpath, file) + Logger.debug(`[CoverController] Removing old cover from metadata "${filepath}"`) + await this.removeFile(filepath) + } + } + } + + async checkFileIsValidImage(imagepath) { + const buffer = await readChunk(imagepath, 0, 12) + const imgType = imageType(buffer) + if (!imgType) { + await this.removeFile(imagepath) + return { + error: 'Invalid image' + } + } + + if (!globals.SupportedImageTypes.includes(imgType.ext)) { + await this.removeFile(imagepath) + return { + error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})` + } + } + return imgType + } + + async uploadCover(audiobook, coverFile) { + var extname = Path.extname(coverFile.name.toLowerCase()) + if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) { + return { + error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})` + } + } + + var { fullPath, relPath } = this.getCoverDirectory(audiobook) + await fs.ensureDir(fullPath) + var isStoringInMetadata = relPath.slice(1).startsWith('metadata') + + var coverFilename = `cover${extname}` + var coverFullPath = Path.join(fullPath, coverFilename) + var coverPath = Path.join(relPath, coverFilename) + + + if (isStoringInMetadata) { + await this.checkBookMetadataCovers(fullPath, extname) + } + + // Move cover from temp upload dir to destination + var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => { + Logger.error('[CoverController] Failed to move cover file', path, error) + return false + }) + + if (!success) { + // return res.status(500).send('Failed to move cover into destination') + return { + error: 'Failed to move cover into destination' + } + } + + Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`) + + audiobook.updateBookCover(coverPath) + return { + cover: coverPath + } + } + + async downloadFile(url, filepath) { + Logger.debug(`[CoverController] Starting file download to ${filepath}`) + const writer = fs.createWriteStream(filepath) + const response = await axios({ + url, + method: 'GET', + responseType: 'stream' + }) + response.data.pipe(writer) + return new Promise((resolve, reject) => { + writer.on('finish', resolve) + writer.on('error', reject) + }) + } + + async downloadCoverFromUrl(audiobook, url) { + try { + var { fullPath, relPath } = this.getCoverDirectory(audiobook) + await fs.ensureDir(fullPath) + + var temppath = Path.join(fullPath, 'cover') + var success = await this.downloadFile(url, temppath).then(() => true).catch((err) => { + Logger.error(`[CoverController] Download image file failed for "${url}"`, err) + return false + }) + if (!success) { + return { + error: 'Failed to download image from url' + } + } + + var imgtype = await this.checkFileIsValidImage(temppath) + + if (imgtype.error) { + return imgtype + } + + var coverFilename = `cover.${imgtype.ext}` + var coverPath = Path.join(relPath, coverFilename) + var coverFullPath = Path.join(fullPath, coverFilename) + await fs.rename(temppath, coverFullPath) + + var isStoringInMetadata = relPath.slice(1).startsWith('metadata') + if (isStoringInMetadata) { + await this.checkBookMetadataCovers(fullPath, '.' + imgtype.ext) + } + + Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`) + + audiobook.updateBookCover(coverPath) + return { + cover: coverPath + } + } catch (error) { + Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error) + return { + error: 'Failed to fetch image from url' + } + } + } +} +module.exports = CoverController \ No newline at end of file diff --git a/server/Scanner.js b/server/Scanner.js index 8226daa7..44a7b78a 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -106,14 +106,19 @@ class Scanner { // check an audiobook exists with matching path, then update inodes existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path) if (existingAudiobook) { + existingAudiobook.ino = audiobookData.ino hasUpdatedIno = true - var filesUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData) - Logger.info(`[Scanner] Updating inode value for "${existingAudiobook.title}" - ${filesUpdated} files updated`) } } - // Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`) if (existingAudiobook) { + // Always sync files and inode values + var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData) + if (hasUpdatedIno || filesInodeUpdated > 0) { + Logger.info(`[Scanner] Updating inode value for "${existingAudiobook.title}" - ${filesInodeUpdated} files updated`) + hasUpdatedIno = true + } + // TEMP: Check if is older audiobook and needs force rescan if (!forceAudioFileScan && existingAudiobook.checkNeedsAudioFileRescan()) { diff --git a/server/Server.js b/server/Server.js index 64f2bb83..b47e5ab1 100644 --- a/server/Server.js +++ b/server/Server.js @@ -17,6 +17,7 @@ const HlsController = require('./HlsController') const StreamManager = require('./StreamManager') const RssFeeds = require('./RssFeeds') const DownloadManager = require('./DownloadManager') +const CoverController = require('./CoverController') // const EbookReader = require('./EbookReader') const Logger = require('./Logger') @@ -38,9 +39,11 @@ class Server { this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this)) this.streamManager = new StreamManager(this.db, this.MetadataPath) this.rssFeeds = new RssFeeds(this.Port, this.db) + this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath) this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this)) - this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.emitter.bind(this), this.clientEmitter.bind(this)) + this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.emitter.bind(this), this.clientEmitter.bind(this)) this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath) + // this.ebookReader = new EbookReader(this.db, this.MetadataPath, this.AudiobookPath) this.server = null @@ -132,6 +135,33 @@ class Server { socket.emit('save_metadata_complete', response) } + // Remove unused /metadata/books/{id} folders + async purgeMetadata() { + var booksMetadata = Path.join(this.MetadataPath, 'books') + var booksMetadataExists = await fs.pathExists(booksMetadata) + if (!booksMetadataExists) return + var foldersInBooksMetadata = await fs.readdir(booksMetadata) + + var purged = 0 + await Promise.all(foldersInBooksMetadata.map(async foldername => { + var hasMatchingAudiobook = this.audiobooks.find(ab => ab.id === foldername) + if (!hasMatchingAudiobook) { + var folderPath = Path.join(booksMetadata, foldername) + Logger.debug(`[Server] Purging unused metadata ${folderPath}`) + + await fs.remove(folderPath).then(() => { + purged++ + }).catch((err) => { + Logger.error(`[Server] Failed to delete folder path ${folderPath}`, err) + }) + } + })) + if (purged > 0) { + Logger.info(`[Server] Purged ${purged} unused audiobook metadata`) + } + return purged + } + async init() { Logger.info('[Server] Init') await this.streamManager.ensureStreamsDir() @@ -141,6 +171,8 @@ class Server { await this.db.init() this.auth.init() + await this.purgeMetadata() + this.watcher.initWatcher() this.watcher.on('files', this.filesChanged.bind(this)) } diff --git a/server/objects/AudioTrack.js b/server/objects/AudioTrack.js index c6306dee..18f8044e 100644 --- a/server/objects/AudioTrack.js +++ b/server/objects/AudioTrack.js @@ -62,6 +62,7 @@ class AudioTrack { size: this.size, bitRate: this.bitRate, language: this.language, + codec: this.codec, timeBase: this.timeBase, channels: this.channels, channelLayout: this.channelLayout, @@ -82,7 +83,7 @@ class AudioTrack { this.size = probeData.size this.bitRate = probeData.bitRate this.language = probeData.language - this.codec = probeData.codec + this.codec = probeData.codec || null this.timeBase = probeData.timeBase this.channels = probeData.channels this.channelLayout = probeData.channelLayout diff --git a/server/utils/globals.js b/server/utils/globals.js new file mode 100644 index 00000000..cb5874ac --- /dev/null +++ b/server/utils/globals.js @@ -0,0 +1,7 @@ +const globals = { + SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], + SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac'], + SupportedEbookTypes: ['epub', 'pdf', 'mobi'] +} + +module.exports = globals diff --git a/server/utils/index.js b/server/utils/index.js index 944eb39d..781fdcde 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -63,7 +63,3 @@ module.exports.getIno = (path) => { return null }) } - -module.exports.isAcceptableCoverMimeType = (mimeType) => { - return mimeType && mimeType.startsWith('image/') -} \ No newline at end of file diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 279fcf0a..5fcec371 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -2,11 +2,7 @@ const Path = require('path') const dir = require('node-dir') const Logger = require('../Logger') const { getIno } = require('./index') - -const AUDIO_FORMATS = ['m4b', 'mp3', 'm4a', 'flac'] -const INFO_FORMATS = ['nfo'] -const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp'] -const EBOOK_FORMATS = ['epub', 'pdf', 'mobi'] +const globals = require('./globals') function getPaths(path) { return new Promise((resolve) => { @@ -24,7 +20,7 @@ function isAudioFile(path) { if (!path) return false var ext = Path.extname(path) if (!ext) return false - return AUDIO_FORMATS.includes(ext.slice(1).toLowerCase()) + return globals.SupportedAudioTypes.includes(ext.slice(1).toLowerCase()) } function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) { @@ -107,10 +103,10 @@ function cleanFileObjects(basepath, abrelpath, files) { function getFileType(ext) { var ext_cleaned = ext.toLowerCase() if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1) - if (AUDIO_FORMATS.includes(ext_cleaned)) return 'audio' - if (INFO_FORMATS.includes(ext_cleaned)) return 'info' - if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image' - if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook' + if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio' + if (ext_cleaned === 'nfo') return 'info' + if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image' + if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook' return 'unknown' }