diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 24d3d68d..6baf5dd7 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -157,6 +157,17 @@ +
+ + +

+ Max # of threads to use + info_outlined +

+
+ +
+

Experimental Features

@@ -184,6 +195,16 @@

+ +
+ + +

+ Scanner use old single threaded audio prober + info_outlined +

+
+
@@ -268,7 +289,9 @@ export default { storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension', coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers', enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)', - scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically' + scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically', + scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.', + scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage.

Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)' }, showConfirmPurgeCache: false } @@ -300,6 +323,26 @@ export default { } }, methods: { + updateScannerMaxThreads(val) { + if (!val || isNaN(val)) { + this.$toast.error('Invalid max threads must be a number') + this.newServerSettings.scannerMaxThreads = 0 + return + } + if (Number(val) < 0) { + this.$toast.error('Max threads must be >= 0') + this.newServerSettings.scannerMaxThreads = 0 + return + } + if (Math.round(Number(val)) !== Number(val)) { + this.$toast.error('Max threads must be an integer') + this.newServerSettings.scannerMaxThreads = 0 + return + } + this.updateServerSettings({ + scannerMaxThreads: Number(val) + }) + }, updateSortingPrefixes(val) { if (!val || !val.length) { this.$toast.error('Must have at least 1 prefix') diff --git a/server/Db.js b/server/Db.js index f9fac6d4..ceb80c53 100644 --- a/server/Db.js +++ b/server/Db.js @@ -10,7 +10,6 @@ const Author = require('./objects/entities/Author') const Series = require('./objects/entities/Series') const ServerSettings = require('./objects/settings/ServerSettings') const PlaybackSession = require('./objects/PlaybackSession') -const Feed = require('./objects/Feed') class Db { constructor() { diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index 27c62e8e..35cb61b0 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -136,23 +136,6 @@ class AudioFile { this.embeddedCoverArt = probeData.embeddedCoverArt } - validateTrackIndex() { - var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta) - var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename) - - if (numFromMeta !== null) return numFromMeta - if (numFromFilename !== null) return numFromFilename - - this.invalid = true - this.error = 'Failed to get track number' - return null - } - - setDuplicateTrackNumber(num) { - this.invalid = true - this.error = 'Duplicate track number "' + num + '"' - } - syncChapters(updatedChapters) { if (this.chapters.length !== updatedChapters.length) { this.chapters = updatedChapters.map(ch => ({ ...ch })) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 71c47828..4860d494 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -1,4 +1,5 @@ const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants') +const { isNullOrNaN } = require('../../utils') const Logger = require('../../Logger') class ServerSettings { @@ -14,6 +15,8 @@ class ServerSettings { this.scannerPreferMatchedMetadata = false this.scannerDisableWatcher = false this.scannerPreferOverdriveMediaMarker = false + this.scannerUseSingleThreadedProber = false + this.scannerMaxThreads = 0 // 0 = defaults to CPUs * 2 // Metadata - choose to store inside users library item folder this.storeCoverWithItem = false @@ -68,6 +71,8 @@ class ServerSettings { this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata this.scannerDisableWatcher = !!settings.scannerDisableWatcher this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker + this.scannerUseSingleThreadedProber = !!settings.scannerUseSingleThreadedProber + this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads) this.storeCoverWithItem = !!settings.storeCoverWithItem if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2 @@ -116,6 +121,8 @@ class ServerSettings { scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata, scannerDisableWatcher: this.scannerDisableWatcher, scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker, + scannerUseSingleThreadedProber: this.scannerUseSingleThreadedProber, + scannerMaxThreads: this.scannerMaxThreads, storeCoverWithItem: this.storeCoverWithItem, storeMetadataWithItem: this.storeMetadataWithItem, rateLimitLoginRequests: this.rateLimitLoginRequests, diff --git a/server/scanner/MediaFileScanner.js b/server/scanner/MediaFileScanner.js index e010d1b1..38b74e0d 100644 --- a/server/scanner/MediaFileScanner.js +++ b/server/scanner/MediaFileScanner.js @@ -3,6 +3,8 @@ const Path = require('path') const AudioFile = require('../objects/files/AudioFile') const VideoFile = require('../objects/files/VideoFile') +const MediaProbePool = require('./MediaProbePool') + const prober = require('../utils/prober') const Logger = require('../Logger') const { LogLevel } = require('../utils/constants') @@ -100,19 +102,38 @@ class MediaFileScanner { } // Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects - async executeMediaFileScans(mediaType, mediaLibraryFiles, scanData) { - var mediaMetadataFromScan = scanData.media.metadata || null - var proms = [] - for (let i = 0; i < mediaLibraryFiles.length; i++) { - proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan)) - } - var scanStart = Date.now() - var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr)) - return { - audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile), - videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile), - elapsed: Date.now() - scanStart, - averageScanDuration: this.getAverageScanDurationMs(results) + async executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) { + const mediaType = libraryItem.mediaType + + if (!global.ServerSettings.scannerUseSingleThreadedProber) { // New multi-threaded scanner + var scanStart = Date.now() + const probeResults = await new Promise((resolve) => { + // const probePool = new MediaProbePool(mediaType, mediaLibraryFiles, scanData, global.ServerSettings.scannerMaxThreads) + const itemBatch = MediaProbePool.initBatch(libraryItem, mediaLibraryFiles, scanData) + itemBatch.on('done', resolve) + MediaProbePool.runBatch(itemBatch) + }) + + return { + audioFiles: probeResults.audioFiles || [], + videoFiles: probeResults.videoFiles || [], + elapsed: Date.now() - scanStart, + averageScanDuration: probeResults.averageTimePerMb + } + } else { // Old single threaded scanner + var scanStart = Date.now() + var mediaMetadataFromScan = scanData.media.metadata || null + var proms = [] + for (let i = 0; i < mediaLibraryFiles.length; i++) { + proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan)) + } + var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr)) + return { + audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile), + videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile), + elapsed: Date.now() - scanStart, + averageScanDuration: this.getAverageScanDurationMs(results) + } } } @@ -149,7 +170,6 @@ class MediaFileScanner { if (af.discNumFromMeta !== null) discsFromMeta.push(af.discNumFromMeta) if (af.trackNumFromFilename !== null) tracksFromFilename.push(af.trackNumFromFilename) if (af.trackNumFromMeta !== null) tracksFromMeta.push(af.trackNumFromMeta) - af.validateTrackIndex() // Sets error if no valid track number }) discsFromFilename.sort((a, b) => a - b) discsFromMeta.sort((a, b) => a - b) @@ -198,7 +218,8 @@ class MediaFileScanner { async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) { var hasUpdated = false - var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData) + var mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) + if (libraryItem.mediaType === 'video') { if (mediaScanResult.videoFiles.length) { // TODO: Check for updates etc @@ -207,9 +228,9 @@ class MediaFileScanner { } } else if (mediaScanResult.audioFiles.length) { if (libraryScan) { - libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`) - Logger.debug(`Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`) + libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms per MB`) } + Logger.debug(`Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms with ${mediaScanResult.audioFiles.length} audio files averaging ${mediaScanResult.averageScanDuration}ms per MB`) var newAudioFiles = mediaScanResult.audioFiles.filter(af => { return !libraryItem.media.findFileWithInode(af.ino) diff --git a/server/scanner/MediaProbeData.js b/server/scanner/MediaProbeData.js index 2946077c..88edb4d7 100644 --- a/server/scanner/MediaProbeData.js +++ b/server/scanner/MediaProbeData.js @@ -1,7 +1,7 @@ const AudioFileMetadata = require('../objects/metadata/AudioMetaTags') class MediaProbeData { - constructor() { + constructor(probeData) { this.embeddedCoverArt = null this.format = null this.duration = null @@ -26,6 +26,20 @@ class MediaProbeData { this.discNumber = null this.discTotal = null + + if (probeData) { + this.construct(probeData) + } + } + + construct(probeData) { + for (const key in probeData) { + if (key === 'audioFileMetadata' && probeData[key]) { + this[key] = new AudioFileMetadata(probeData[key]) + } else if (this[key] !== undefined) { + this[key] = probeData[key] + } + } } getEmbeddedCoverArt(videoStream) { diff --git a/server/scanner/MediaProbePool.js b/server/scanner/MediaProbePool.js new file mode 100644 index 00000000..4076c809 --- /dev/null +++ b/server/scanner/MediaProbePool.js @@ -0,0 +1,209 @@ +const os = require('os') +const Path = require('path') +const { EventEmitter } = require('events') +const { Worker } = require("worker_threads") +const Logger = require('../Logger') +const AudioFile = require('../objects/files/AudioFile') +const VideoFile = require('../objects/files/VideoFile') +const MediaProbeData = require('./MediaProbeData') + +class LibraryItemBatch extends EventEmitter { + constructor(libraryItem, libraryFiles, scanData) { + super() + + this.id = libraryItem.id + this.mediaType = libraryItem.mediaType + this.mediaMetadataFromScan = scanData.media.metadata || null + this.libraryFilesToScan = libraryFiles + + // Results + this.totalElapsed = 0 + this.totalProbed = 0 + this.audioFiles = [] + this.videoFiles = [] + } + + done() { + this.emit('done', { + videoFiles: this.videoFiles, + audioFiles: this.audioFiles, + averageTimePerMb: Math.round(this.totalElapsed / this.totalProbed) + }) + } +} + +class MediaProbePool { + constructor() { + this.MaxThreads = 0 + this.probeWorkerScript = null + + this.itemBatchMap = {} + + this.probesRunning = [] + this.probeQueue = [] + } + + tick() { + if (this.probesRunning.length < this.MaxThreads) { + if (this.probeQueue.length > 0) { + const pw = this.probeQueue.shift() + // console.log('Unqueued probe - Remaining is', this.probeQueue.length, 'Currently running is', this.probesRunning.length) + this.startTask(pw) + } else if (!this.probesRunning.length) { + // console.log('No more probes to run') + } + } + } + + async startTask(task) { + this.probesRunning.push(task) + + const itemBatch = this.itemBatchMap[task.batchId] + + await task.start().then((taskResult) => { + itemBatch.libraryFilesToScan = itemBatch.libraryFilesToScan.filter(lf => lf.ino !== taskResult.libraryFile.ino) + + var fileSizeMb = taskResult.libraryFile.metadata.size / (1024 * 1024) + var elapsedPerMb = Math.round(taskResult.elapsed / fileSizeMb) + + const probeData = new MediaProbeData(taskResult.data) + + if (itemBatch.mediaType === 'video') { + if (!probeData.videoStream) { + Logger.error('[MediaProbePool] Invalid video file no video stream') + } else { + itemBatch.totalElapsed += elapsedPerMb + itemBatch.totalProbed++ + + var videoFile = new VideoFile() + videoFile.setDataFromProbe(libraryFile, probeData) + itemBatch.videoFiles.push(videoFile) + } + } else { + if (!probeData.audioStream) { + Logger.error('[MediaProbePool] Invalid audio file no audio stream') + } else { + itemBatch.totalElapsed += elapsedPerMb + itemBatch.totalProbed++ + + var audioFile = new AudioFile() + audioFile.trackNumFromMeta = probeData.trackNumber + audioFile.discNumFromMeta = probeData.discNumber + if (itemBatch.mediaType === 'book') { + const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(itemBatch.mediaMetadataFromScan, taskResult.libraryFile) + audioFile.trackNumFromFilename = trackNumber + audioFile.discNumFromFilename = discNumber + } + audioFile.setDataFromProbe(taskResult.libraryFile, probeData) + + itemBatch.audioFiles.push(audioFile) + } + } + + this.probesRunning = this.probesRunning.filter(tq => tq.mediaPath !== task.mediaPath) + this.tick() + }).catch((error) => { + itemBatch.libraryFilesToScan = itemBatch.libraryFilesToScan.filter(lf => lf.ino !== taskResult.libraryFile.ino) + + Logger.error('[MediaProbePool] Task failed', error) + this.probesRunning = this.probesRunning.filter(tq => tq.mediaPath !== task.mediaPath) + this.tick() + }) + + if (!itemBatch.libraryFilesToScan.length) { + itemBatch.done() + delete this.itemBatchMap[itemBatch.id] + } + } + + buildTask(libraryFile, batchId) { + return { + batchId, + mediaPath: libraryFile.metadata.path, + start: () => { + return new Promise((resolve, reject) => { + const startTime = Date.now() + + const worker = new Worker(this.probeWorkerScript) + worker.on("message", ({ data }) => { + if (data.error) { + reject(data.error) + } else { + resolve({ + data, + elapsed: Date.now() - startTime, + libraryFile + }) + } + }) + worker.postMessage({ + mediaPath: libraryFile.metadata.path + }) + }) + } + } + } + + initBatch(libraryItem, libraryFiles, scanData) { + this.MaxThreads = global.ServerSettings.scannerMaxThreads || (os.cpus().length * 2) + this.probeWorkerScript = Path.join(global.appRoot, 'server/utils/probeWorker.js') + + Logger.debug(`[MediaProbePool] Run item batch ${libraryItem.id} with`, libraryFiles.length, 'files and max concurrent of', this.MaxThreads) + + const itemBatch = new LibraryItemBatch(libraryItem, libraryFiles, scanData) + this.itemBatchMap[itemBatch.id] = itemBatch + + return itemBatch + } + + runBatch(itemBatch) { + for (const libraryFile of itemBatch.libraryFilesToScan) { + const probeTask = this.buildTask(libraryFile, itemBatch.id) + + if (this.probesRunning.length < this.MaxThreads) { + this.startTask(probeTask) + } else { + this.probeQueue.push(probeTask) + } + } + } + + getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) { + const { title, author, series, publishedYear } = mediaMetadataFromScan + const { filename, path } = audioLibraryFile.metadata + var partbasename = Path.basename(filename, Path.extname(filename)) + + // Remove title, author, series, and publishedYear from filename if there + if (title) partbasename = partbasename.replace(title, '') + if (author) partbasename = partbasename.replace(author, '') + if (series) partbasename = partbasename.replace(series, '') + if (publishedYear) partbasename = partbasename.replace(publishedYear) + + // Look for disc number + var discNumber = null + var discMatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i) + if (discMatch && discMatch.length > 2 && discMatch[2]) { + if (!isNaN(discMatch[2])) { + discNumber = Number(discMatch[2]) + } + + // Remove disc number from filename + partbasename = partbasename.replace(/\b(disc|cd) ?(\d\d?)\b/i, '') + } + + // Look for disc number in folder path e.g. /Book Title/CD01/audiofile.mp3 + var pathdir = Path.dirname(path).split('/').pop() + if (pathdir && /^cd\d{1,3}$/i.test(pathdir)) { + var discFromFolder = Number(pathdir.replace(/cd/i, '')) + if (!isNaN(discFromFolder) && discFromFolder !== null) discNumber = discFromFolder + } + + var numbersinpath = partbasename.match(/\d{1,4}/g) + var trackNumber = numbersinpath && numbersinpath.length ? parseInt(numbersinpath[0]) : null + return { + trackNumber, + discNumber + } + } +} +module.exports = new MediaProbePool() \ No newline at end of file diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index d8bdd68f..8c49722f 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -205,20 +205,27 @@ class Scanner { checkRes.libraryItem = libraryItem checkRes.scanData = dataFound - // If this item will go over max size then push current chunk - if (libraryItem.audioFileTotalSize + itemDataToRescanSize > MaxSizePerChunk && itemDataToRescan.length > 0) { - itemDataToRescanChunks.push(itemDataToRescan) - itemDataToRescanSize = 0 - itemDataToRescan = [] + console.log('Has New Library Files', libraryItem.media.metadata.title, 'num new', checkRes.newLibraryFiles.length) + + if (global.ServerSettings.scannerUseSingleThreadedProber) { + // If this item will go over max size then push current chunk + if (libraryItem.audioFileTotalSize + itemDataToRescanSize > MaxSizePerChunk && itemDataToRescan.length > 0) { + itemDataToRescanChunks.push(itemDataToRescan) + itemDataToRescanSize = 0 + itemDataToRescan = [] + } + + itemDataToRescan.push(checkRes) + itemDataToRescanSize += libraryItem.audioFileTotalSize + if (itemDataToRescanSize >= MaxSizePerChunk) { + itemDataToRescanChunks.push(itemDataToRescan) + itemDataToRescanSize = 0 + itemDataToRescan = [] + } + } else { + itemDataToRescan.push(checkRes) } - itemDataToRescan.push(checkRes) - itemDataToRescanSize += libraryItem.audioFileTotalSize - if (itemDataToRescanSize >= MaxSizePerChunk) { - itemDataToRescanChunks.push(itemDataToRescan) - itemDataToRescanSize = 0 - itemDataToRescan = [] - } } else if (libraryScan.findCovers && libraryItem.media.shouldSearchForCover) { // Search cover libraryScan.resultsUpdated++ itemsToFindCovers.push(libraryItem) @@ -235,27 +242,31 @@ class Scanner { // Potential NEW Library Items for (let i = 0; i < libraryItemDataFound.length; i++) { var dataFound = libraryItemDataFound[i] - + console.log('Potential new library item data') var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile) if (!hasMediaFile) { libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`) } else { - var mediaFileSize = 0 - dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size) + if (global.ServerSettings.scannerUseSingleThreadedProber) { + // If this item will go over max size then push current chunk + var mediaFileSize = 0 + dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size) + if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) { + newItemDataToScanChunks.push(newItemDataToScan) + newItemDataToScanSize = 0 + newItemDataToScan = [] + } - // If this item will go over max size then push current chunk - if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) { - newItemDataToScanChunks.push(newItemDataToScan) - newItemDataToScanSize = 0 - newItemDataToScan = [] - } + newItemDataToScan.push(dataFound) + newItemDataToScanSize += mediaFileSize - newItemDataToScan.push(dataFound) - newItemDataToScanSize += mediaFileSize - if (newItemDataToScanSize >= MaxSizePerChunk) { - newItemDataToScanChunks.push(newItemDataToScan) - newItemDataToScanSize = 0 - newItemDataToScan = [] + if (newItemDataToScanSize >= MaxSizePerChunk) { + newItemDataToScanChunks.push(newItemDataToScan) + newItemDataToScanSize = 0 + newItemDataToScan = [] + } + } else { // Chunking is not necessary for new scanner + newItemDataToScan.push(dataFound) } } } @@ -272,14 +283,14 @@ class Scanner { await this.updateLibraryItemChunk(itemsToUpdate) if (this.cancelLibraryScan[libraryScan.libraryId]) return true } + + // Chunking will be removed when legacy single threaded scanner is removed for (let i = 0; i < itemDataToRescanChunks.length; i++) { await this.rescanLibraryItemDataChunk(itemDataToRescanChunks[i], libraryScan) if (this.cancelLibraryScan[libraryScan.libraryId]) return true - // console.log('Rescan chunk done', i, 'of', itemDataToRescanChunks.length) } for (let i = 0; i < newItemDataToScanChunks.length; i++) { await this.scanNewLibraryItemDataChunk(newItemDataToScanChunks[i], libraryScan) - // console.log('New scan chunk done', i, 'of', newItemDataToScanChunks.length) if (this.cancelLibraryScan[libraryScan.libraryId]) return true } } diff --git a/server/utils/probeWorker.js b/server/utils/probeWorker.js new file mode 100644 index 00000000..2a93fce9 --- /dev/null +++ b/server/utils/probeWorker.js @@ -0,0 +1,9 @@ +const { parentPort } = require("worker_threads") +const prober = require('./prober') + +parentPort.on("message", async ({ mediaPath }) => { + const results = await prober.probe(mediaPath) + parentPort.postMessage({ + data: results, + }) +}) \ No newline at end of file diff --git a/server/utils/prober.js b/server/utils/prober.js index ec047fbf..770c235e 100644 --- a/server/utils/prober.js +++ b/server/utils/prober.js @@ -220,19 +220,18 @@ function getDefaultAudioStream(audioStreams) { function parseProbeData(data, verbose = false) { try { var { format, streams, chapters } = data - var { format_long_name, duration, size, bit_rate } = format - var sizeBytes = !isNaN(size) ? Number(size) : null + var sizeBytes = !isNaN(format.size) ? Number(format.size) : null var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null // Logger.debug('Parsing Data for', Path.basename(format.filename)) var tags = parseTags(format, verbose) var cleanedData = { - format: format_long_name, - duration: !isNaN(duration) ? Number(duration) : null, + format: format.format_long_name || format.name || 'Unknown', + duration: !isNaN(format.duration) ? Number(format.duration) : null, size: sizeBytes, sizeMb, - bit_rate: !isNaN(bit_rate) ? Number(bit_rate) : null, + bit_rate: !isNaN(format.bit_rate) ? Number(format.bit_rate) : null, ...tags } if (verbose && format.tags) { @@ -278,6 +277,12 @@ function probe(filepath, verbose = false) { return ffprobe(filepath) .then(raw => { + if (raw.error) { + return { + error: raw.error.string + } + } + var rawProbeData = parseProbeData(raw, verbose) if (!rawProbeData || (!rawProbeData.audio_stream && !rawProbeData.video_stream)) { return {