mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Update:Scanner adjustable number of parallel audio probes to use less CPU
This commit is contained in:
parent
277a5fa37c
commit
86ee4dcff2
@ -157,6 +157,17 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
|
||||
<ui-tooltip :text="tooltips.scannerMaxThreads">
|
||||
<p class="pl-4">
|
||||
Max # of threads to use
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
<!-- <p class="pl-4 text-sm"></p> -->
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">Experimental Features</h2>
|
||||
</div>
|
||||
@ -184,6 +195,16 @@
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.scannerUseSingleThreadedProber" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseSingleThreadedProber', val)" />
|
||||
<ui-tooltip :text="tooltips.scannerUseSingleThreadedProber">
|
||||
<p class="pl-4">
|
||||
Scanner use old single threaded audio prober
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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. <br><br>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')
|
||||
|
@ -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() {
|
||||
|
@ -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 }))
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
209
server/scanner/MediaProbePool.js
Normal file
209
server/scanner/MediaProbePool.js
Normal file
@ -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()
|
@ -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
|
||||
}
|
||||
}
|
||||
|
9
server/utils/probeWorker.js
Normal file
9
server/utils/probeWorker.js
Normal file
@ -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,
|
||||
})
|
||||
})
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user