diff --git a/Dockerfile b/Dockerfile
index 47aced8a..ddafa0a4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,9 @@ RUN npm ci && npm cache clean --force
RUN npm run generate
### STAGE 1: Build server ###
+FROM sandreas/tone:v0.0.9 AS tone
FROM node:16-alpine
+
ENV NODE_ENV=production
RUN apk update && \
apk add --no-cache --update \
@@ -14,6 +16,7 @@ RUN apk update && \
tzdata \
ffmpeg
+COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
COPY index.js package* /
COPY server server
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 8e73e1ab..ad79caff 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -172,17 +172,15 @@
@@ -195,15 +193,15 @@
-
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 43acbfac..de1a3e52 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,11 +3,11 @@ version: "3.7"
services:
audiobookshelf:
- image: ghcr.io/advplyr/audiobookshelf
+ image: audiobookshelf-test
ports:
- 13378:80
volumes:
- - ./audiobooks:/audiobooks
- - ./metadata:/metadata
- - ./config:/config
+ - ./media/audiobooks:/audiobooks
+ - ./test/metadata:/metadata
+ - ./test/config:/config
restart: unless-stopped
diff --git a/package-lock.json b/package-lock.json
index 6388098a..17bf4bfb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"express": "^4.17.1",
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
+ "node-tone": "^1.0.1",
"socket.io": "^4.4.1",
"xml2js": "^0.4.23"
},
@@ -594,6 +595,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-tone": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
+ "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1360,6 +1366,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
+ "node-tone": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
+ "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
+ },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1602,4 +1613,4 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 5fc15ffd..25bebfa4 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"express": "^4.17.1",
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
+ "node-tone": "^1.0.1",
"socket.io": "^4.4.1",
"xml2js": "^0.4.23"
}
-}
\ No newline at end of file
+}
diff --git a/server/objects/metadata/AudioMetaTags.js b/server/objects/metadata/AudioMetaTags.js
index 13bb2a83..7f1486b7 100644
--- a/server/objects/metadata/AudioMetaTags.js
+++ b/server/objects/metadata/AudioMetaTags.js
@@ -87,6 +87,10 @@ class AudioMetaTags {
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
}
+ setDataFromTone(tags) {
+ // TODO: Implement
+ }
+
updateData(payload) {
const dataMap = {
tagAlbum: payload.file_tag_album || null,
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index 0f701f50..85efb70c 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -17,7 +17,8 @@ class ServerSettings {
this.scannerDisableWatcher = false
this.scannerPreferOverdriveMediaMarker = false
this.scannerUseSingleThreadedProber = true
- this.scannerMaxThreads = 0 // 0 = defaults to CPUs * 2
+ this.scannerMaxThreads = 0 // Currently not being used
+ this.scannerUseTone = false
// Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false
@@ -82,6 +83,7 @@ class ServerSettings {
this.scannerUseSingleThreadedProber = true
}
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
+ this.scannerUseTone = !!settings.scannerUseTone
this.storeCoverWithItem = !!settings.storeCoverWithItem
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
@@ -139,6 +141,7 @@ class ServerSettings {
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
scannerUseSingleThreadedProber: this.scannerUseSingleThreadedProber,
scannerMaxThreads: this.scannerMaxThreads,
+ scannerUseTone: this.scannerUseTone,
storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem,
rateLimitLoginRequests: this.rateLimitLoginRequests,
diff --git a/server/scanner/MediaFileScanner.js b/server/scanner/MediaFileScanner.js
index 38b74e0d..c57d30b2 100644
--- a/server/scanner/MediaFileScanner.js
+++ b/server/scanner/MediaFileScanner.js
@@ -3,9 +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 toneProber = require('../utils/toneProber')
const Logger = require('../Logger')
const { LogLevel } = require('../utils/constants')
@@ -59,7 +58,15 @@ class MediaFileScanner {
async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
var probeStart = Date.now()
- var probeData = await prober.probe(libraryFile.metadata.path, verbose)
+
+ var probeData = null
+ if (global.ServerSettings.scannerUseTone) {
+ Logger.debug(`[MediaFileScanner] using tone to probe audio file "${libraryFile.metadata.path}"`)
+ probeData = await toneProber.probe(libraryFile.metadata.path, true)
+ } else {
+ probeData = await prober.probe(libraryFile.metadata.path, verbose)
+ }
+
if (probeData.error) {
Logger.error(`[MediaFileScanner] ${probeData.error} : "${libraryFile.metadata.path}"`)
return null
@@ -105,35 +112,18 @@ class MediaFileScanner {
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)
- }
+ 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)
}
}
diff --git a/server/scanner/MediaProbeData.js b/server/scanner/MediaProbeData.js
index 88edb4d7..f4ac2b59 100644
--- a/server/scanner/MediaProbeData.js
+++ b/server/scanner/MediaProbeData.js
@@ -66,15 +66,20 @@ class MediaProbeData {
this.sampleRate = audioStream.sample_rate
this.chapters = data.chapters || []
- var metatags = {}
- for (const key in data) {
- if (data[key] && key.startsWith('file_tag')) {
- metatags[key] = data[key]
+ if (data.tags) { // New for tone library data (toneProber.js)
+ this.audioFileMetadata = new AudioFileMetadata()
+ this.audioFileMetadata.setDataFromTone(data.tags)
+ } else { // Data from ffprobe (prober.js)
+ var metatags = {}
+ for (const key in data) {
+ if (data[key] && key.startsWith('file_tag')) {
+ metatags[key] = data[key]
+ }
}
- }
- this.audioFileMetadata = new AudioFileMetadata()
- this.audioFileMetadata.setData(metatags)
+ this.audioFileMetadata = new AudioFileMetadata()
+ this.audioFileMetadata.setData(metatags)
+ }
// Track ID3 tag might be "3/10" or just "3"
if (this.audioFileMetadata.tagTrack) {
diff --git a/server/scanner/MediaProbePool.js b/server/scanner/MediaProbePool.js
deleted file mode 100644
index 4076c809..00000000
--- a/server/scanner/MediaProbePool.js
+++ /dev/null
@@ -1,209 +0,0 @@
-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/utils/prober.js b/server/utils/prober.js
index 770c235e..8b235f9b 100644
--- a/server/utils/prober.js
+++ b/server/utils/prober.js
@@ -200,12 +200,6 @@ function parseTags(format, verbose) {
}
}
- // var keysToLookOutFor = ['file_tag_genre1', 'file_tag_genre2', 'file_tag_series', 'file_tag_seriespart', 'file_tag_movement', 'file_tag_movementname', 'file_tag_wwwaudiofile', 'file_tag_contentgroup', 'file_tag_releasetime', 'file_tag_isbn']
- // keysToLookOutFor.forEach((key) => {
- // if (tags[key]) {
- // Logger.debug(`Notable! ${key} => ${tags[key]}`)
- // }
- // })
return tags
}
diff --git a/server/utils/toneProber.js b/server/utils/toneProber.js
new file mode 100644
index 00000000..7713edb3
--- /dev/null
+++ b/server/utils/toneProber.js
@@ -0,0 +1,158 @@
+const tone = require('node-tone')
+const MediaProbeData = require('../scanner/MediaProbeData')
+const Logger = require('../Logger')
+
+/*
+Sample dump from tone
+{
+ "audio": {
+ "bitrate": 17,
+ "format": "MPEG-4 Part 14",
+ "formatShort": "MPEG-4",
+ "sampleRate": 44100.0,
+ "duration": 209284.0,
+ "channels": {
+ "count": 2,
+ "description": "Stereo (2/0.0)"
+ },
+ "frames": {
+ "offset": 42168,
+ "length": 446932
+ "metaFormat": [
+ "mp4"
+ ]
+ },
+ "meta": {
+ "album": "node-tone",
+ "albumArtist": "advplyr",
+ "artist": "advplyr",
+ "composer": "Composer 5",
+ "comment": "testing out tone metadata",
+ "encodingTool": "audiobookshelf",
+ "genre": "abs",
+ "itunesCompilation": "no",
+ "itunesMediaType": "audiobook",
+ "itunesPlayGap": "noGap",
+ "narrator": "Narrator 5",
+ "recordingDate": "2022-09-10T00:00:00",
+ "title": "Test 5",
+ "trackNumber": 5,
+ "chapters": [
+ {
+ "start": 0,
+ "length": 500,
+ "title": "chapter 1"
+ },
+ {
+ "start": 500,
+ "length": 500,
+ "title": "chapter 2"
+ },
+ {
+ "start": 1000,
+ "length": 208284,
+ "title": "chapter 3"
+ }
+ ],
+ "embeddedPictures": [
+ {
+ "code": 14,
+ "mimetype": "image/png",
+ "data": "..."
+ },
+ "additionalFields": {
+ "test": "Test 5"
+ }
+ },
+ "file": {
+ "size": 530793,
+ "created": "2022-09-10T13:32:51.1942586-05:00",
+ "modified": "2022-09-10T14:09:19.366071-05:00",
+ "accessed": "2022-09-11T13:00:56.5097533-05:00",
+ "path": "C:\\Users\\Coop\\Documents\\NodeProjects\\node-tone\\samples",
+ "name": "m4b.m4b"
+ }
+
+*/
+
+function bitrateKilobitToBit(bitrate) {
+ if (isNaN(bitrate) || !bitrate) return 0
+ return Number(bitrate) * 1000
+}
+
+function msToSeconds(ms) {
+ if (isNaN(ms) || !ms) return 0
+ return Number(ms) / 1000
+}
+
+function parseProbeDump(dumpPayload) {
+ const audioMetadata = dumpPayload.audio
+ const audioChannels = audioMetadata.channels || {}
+ const audio_stream = {
+ bit_rate: bitrateKilobitToBit(audioMetadata.bitrate), // tone uses Kbps but ffprobe uses bps so convert to bits
+ codec: null,
+ time_base: null,
+ language: null,
+ channel_layout: audioChannels.description || null,
+ channels: audioChannels.count || null,
+ sample_rate: audioMetadata.sampleRate || null
+ }
+
+ let chapterIndex = 0
+ const chapters = (dumpPayload.meta.chapters || []).map(chap => {
+ return {
+ id: chapterIndex++,
+ start: msToSeconds(chap.start),
+ end: msToSeconds(chap.start + chap.length),
+ title: chap.title || ''
+ }
+ })
+
+ var video_stream = null
+ if (dumpPayload.meta.embeddedPictures && dumpPayload.meta.embeddedPictures.length) {
+ const mimetype = dumpPayload.meta.embeddedPictures[0].mimetype
+ video_stream = {
+ codec: mimetype === 'image/png' ? 'png' : 'jpeg'
+ }
+ }
+
+ const tags = { ...dumpPayload.meta }
+ delete tags.chapters
+ delete tags.embeddedPictures
+
+ const fileMetadata = dumpPayload.file
+ var sizeBytes = !isNaN(fileMetadata.size) ? Number(fileMetadata.size) : null
+ var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
+ return {
+ format: audioMetadata.format || 'Unknown',
+ duration: msToSeconds(audioMetadata.duration),
+ size: sizeBytes,
+ sizeMb,
+ bit_rate: audio_stream.bit_rate,
+ audio_stream,
+ video_stream,
+ chapters,
+ tags
+ }
+}
+
+module.exports.probe = (filepath, verbose = false) => {
+ if (process.env.TONE_PATH) {
+ ffprobe.TONE_PATH = process.env.TONE_PATH
+ }
+
+ return tone.dump(filepath).then((dumpPayload) => {
+ if (verbose) {
+ Logger.debug(`[toneProber] dump for file "${filepath}"`, dumpPayload)
+ }
+ const rawProbeData = parseProbeDump(dumpPayload)
+ const probeData = new MediaProbeData()
+ probeData.setData(rawProbeData)
+ return probeData
+ }).catch((error) => {
+ Logger.error(`[toneProber] Failed to probe file at path "${filepath}"`, error)
+ return {
+ error
+ }
+ })
+}
\ No newline at end of file