Add:FFProbe api endpoint

This commit is contained in:
advplyr 2023-06-25 16:16:11 -05:00
parent a0e80772cd
commit d0bce2949e
12 changed files with 201 additions and 114 deletions

View File

@ -1,10 +1,15 @@
<template> <template>
<modals-modal v-model="show" name="audiofile-data-modal" :width="700" :height="'unset'"> <modals-modal v-model="show" name="audiofile-data-modal" :width="700" :height="'unset'">
<div v-if="audioFile" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh"> <div v-if="audioFile" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
<p class="text-base text-gray-200">{{ metadata.filename }}</p> <div class="flex items-center justify-between">
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<template v-if="!ffprobeData">
<ui-text-input-with-label :value="metadata.path" readonly :label="$strings.LabelPath" class="mb-4 text-sm" /> <ui-text-input-with-label :value="metadata.path" readonly :label="$strings.LabelPath" class="mb-4 text-sm" />
<div class="flex flex-col sm:flex-row text-sm"> <div class="flex flex-col sm:flex-row text-sm">
@ -80,6 +85,16 @@
</p> </p>
<p>{{ value }}</p> <p>{{ value }}</p>
</div> </div>
</template>
<div v-else class="w-full">
<div class="relative">
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
<span class="material-icons">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
</button>
</div>
</div>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@ -91,10 +106,24 @@ export default {
audioFile: { audioFile: {
type: Object, type: Object,
default: () => {} default: () => {}
} },
libraryItemId: String
}, },
data() { data() {
return {} return {
probingFile: false,
ffprobeData: null,
copiedToClipboard: false
}
},
watch: {
show(newVal) {
if (newVal) {
this.ffprobeData = null
this.copiedToClipboard = false
this.probingFile = false
}
}
}, },
computed: { computed: {
show: { show: {
@ -110,9 +139,36 @@ export default {
}, },
metaTags() { metaTags() {
return this.audioFile?.metaTags || {} return this.audioFile?.metaTags || {}
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
prettyFfprobeData() {
if (!this.ffprobeData) return ''
return JSON.stringify(this.ffprobeData, null, 2)
}
},
methods: {
getFFProbeData() {
this.probingFile = true
this.$axios
.$get(`/api/items/${this.libraryItemId}/ffprobe/${this.audioFile.ino}`)
.then((data) => {
console.log('Got ffprobe data', data)
this.ffprobeData = data
})
.catch((error) => {
console.error('Failed to get ffprobe data', error)
this.$toast.error('FFProbe failed')
})
.finally(() => {
this.probingFile = false
})
},
async copyFfprobeData() {
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
} }
}, },
methods: {},
mounted() {} mounted() {}
} }
</script> </script>

View File

@ -27,7 +27,7 @@
</div> </div>
</transition> </transition>
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :audio-file="selectedAudioFile" /> <modals-audio-file-data-modal v-model="showAudioFileDataModal" :library-item-id="libraryItemId" :audio-file="selectedAudioFile" />
</div> </div>
</template> </template>

View File

@ -33,7 +33,7 @@
</div> </div>
</transition> </transition>
<modals-audio-file-data-modal v-model="showAudioFileDataModal" :audio-file="selectedAudioFile" /> <modals-audio-file-data-modal v-model="showAudioFileDataModal" :library-item-id="libraryItemId" :audio-file="selectedAudioFile" />
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p> <p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" /> <ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :rows="rows" class="w-full" />
</div> </div>
</template> </template>
@ -11,6 +11,7 @@ export default {
value: [String, Number], value: [String, Number],
label: String, label: String,
disabled: Boolean, disabled: Boolean,
readonly: Boolean,
rows: { rows: {
type: Number, type: Number,
default: 2 default: 2

View File

@ -132,8 +132,10 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(str).then(() => { navigator.clipboard.writeText(str).then(() => {
if (ctx) ctx.$toast.success('Copied to clipboard') if (ctx) ctx.$toast.success('Copied to clipboard')
resolve(true)
}, (err) => { }, (err) => {
console.error('Clipboard copy failed', str, err) console.error('Clipboard copy failed', str, err)
resolve(false)
}) })
} else { } else {
const el = document.createElement('textarea') const el = document.createElement('textarea')
@ -147,6 +149,7 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
document.body.removeChild(el) document.body.removeChild(el)
if (ctx) ctx.$toast.success('Copied to clipboard') if (ctx) ctx.$toast.success('Copied to clipboard')
resolve(true)
} }
}) })
} }

View File

@ -472,7 +472,7 @@ class LibraryItemController {
getToneMetadataObject(req, res) { getToneMetadataObject(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user attempted to get tone metadata object`, req.user) Logger.error(`[LibraryItemController] Non-admin user attempted to get tone metadata object`, req.user)
return res.sendStatus(403) return res.sendStatus(403)
} }
@ -514,20 +514,31 @@ class LibraryItemController {
}) })
} }
async toneScan(req, res) { /**
if (!req.libraryItem.media.audioFiles.length) { * GET api/items/:id/ffprobe/:fileid
return res.sendStatus(404) * FFProbe JSON result from audio file
*
* @param {express.Request} req
* @param {express.Response} res
*/
async getFFprobeData(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-admin user attempted to get ffprobe data`, req.user)
return res.sendStatus(403)
}
if (req.libraryFile.fileType !== 'audio') {
Logger.error(`[LibraryItemController] Invalid filetype "${req.libraryFile.fileType}" for fileid "${req.params.fileid}". Expected audio file`)
return res.sendStatus(400)
} }
const audioFileIndex = isNullOrNaN(req.params.index) ? 1 : Number(req.params.index) const audioFile = req.libraryItem.media.findFileWithInode(req.params.fileid)
const audioFile = req.libraryItem.media.audioFiles.find(af => af.index === audioFileIndex)
if (!audioFile) { if (!audioFile) {
Logger.error(`[LibraryItemController] toneScan: Audio file not found with index ${audioFileIndex}`) Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const toneData = await this.scanner.probeAudioFileWithTone(audioFile) const ffprobeData = await this.scanner.probeAudioFile(audioFile)
res.json(toneData) res.json(ffprobeData)
} }
/** /**

View File

@ -198,6 +198,7 @@ class Book {
this.coverPath = coverPath this.coverPath = coverPath
return true return true
} }
removeFileWithInode(inode) { removeFileWithInode(inode) {
if (this.audioFiles.some(af => af.ino === inode)) { if (this.audioFiles.some(af => af.ino === inode)) {
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode) this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
@ -210,8 +211,13 @@ class Book {
return false return false
} }
/**
* Get audio file or ebook file from inode
* @param {string} inode
* @returns {(AudioFile|EBookFile|null)}
*/
findFileWithInode(inode) { findFileWithInode(inode) {
var audioFile = this.audioFiles.find(af => af.ino === inode) const audioFile = this.audioFiles.find(af => af.ino === inode)
if (audioFile) return audioFile if (audioFile) return audioFile
if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
return null return null

View File

@ -15,7 +15,6 @@ class ServerSettings {
this.scannerPreferMatchedMetadata = false this.scannerPreferMatchedMetadata = false
this.scannerDisableWatcher = false this.scannerDisableWatcher = false
this.scannerPreferOverdriveMediaMarker = false this.scannerPreferOverdriveMediaMarker = false
this.scannerUseTone = false
// Metadata - choose to store inside users library item folder // Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false this.storeCoverWithItem = false
@ -72,7 +71,6 @@ class ServerSettings {
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
this.scannerDisableWatcher = !!settings.scannerDisableWatcher this.scannerDisableWatcher = !!settings.scannerDisableWatcher
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
this.scannerUseTone = !!settings.scannerUseTone
this.storeCoverWithItem = !!settings.storeCoverWithItem this.storeCoverWithItem = !!settings.storeCoverWithItem
this.storeMetadataWithItem = !!settings.storeMetadataWithItem this.storeMetadataWithItem = !!settings.storeMetadataWithItem
@ -139,7 +137,6 @@ class ServerSettings {
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata, scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
scannerDisableWatcher: this.scannerDisableWatcher, scannerDisableWatcher: this.scannerDisableWatcher,
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker, scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
scannerUseTone: this.scannerUseTone,
storeCoverWithItem: this.storeCoverWithItem, storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem, storeMetadataWithItem: this.storeMetadataWithItem,
metadataFileFormat: this.metadataFileFormat, metadataFileFormat: this.metadataFileFormat,

View File

@ -121,7 +121,7 @@ class ApiRouter {
this.router.post('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) this.router.post('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this)) this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this))
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this)) this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this)) this.router.get('/items/:id/ffprobe/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getFFprobeData.bind(this))
this.router.get('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getLibraryFile.bind(this)) this.router.get('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getLibraryFile.bind(this))
this.router.delete('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this)) this.router.delete('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this)) this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this))

View File

@ -59,14 +59,7 @@ class MediaFileScanner {
async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) { async scan(mediaType, libraryFile, mediaMetadataFromScan, verbose = false) {
const probeStart = Date.now() const probeStart = Date.now()
let probeData = null const probeData = await prober.probe(libraryFile.metadata.path, verbose)
// TODO: Temp not using tone for probing until more testing can be done
// 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) { if (probeData.error) {
Logger.error(`[MediaFileScanner] ${probeData.error} : "${libraryFile.metadata.path}"`) Logger.error(`[MediaFileScanner] ${probeData.error} : "${libraryFile.metadata.path}"`)
@ -332,9 +325,9 @@ class MediaFileScanner {
return hasUpdated return hasUpdated
} }
probeAudioFileWithTone(audioFile) { probeAudioFile(audioFile) {
Logger.debug(`[MediaFileScanner] using tone to probe audio file "${audioFile.metadata.path}"`) Logger.debug(`[MediaFileScanner] Running ffprobe for audio file at "${audioFile.metadata.path}"`)
return toneProber.rawProbe(audioFile.metadata.path) return prober.rawProbe(audioFile.metadata.path)
} }
} }
module.exports = new MediaFileScanner() module.exports = new MediaFileScanner()

View File

@ -1034,8 +1034,8 @@ class Scanner {
SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
} }
probeAudioFileWithTone(audioFile) { probeAudioFile(audioFile) {
return MediaFileScanner.probeAudioFileWithTone(audioFile) return MediaFileScanner.probeAudioFile(audioFile)
} }
} }
module.exports = Scanner module.exports = Scanner

View File

@ -309,3 +309,23 @@ function probe(filepath, verbose = false) {
}) })
} }
module.exports.probe = probe module.exports.probe = probe
/**
* Ffprobe for audio file path
*
* @param {string} filepath
* @returns {Object} ffprobe json output
*/
function rawProbe(filepath) {
if (process.env.FFPROBE_PATH) {
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
}
return ffprobe(filepath)
.catch((err) => {
return {
error: err
}
})
}
module.exports.rawProbe = rawProbe