2022-01-07 14:15:53 +01:00
|
|
|
const ffprobe = require('node-ffprobe')
|
2021-11-23 02:58:20 +01:00
|
|
|
const AudioProbeData = require('../scanner/AudioProbeData')
|
|
|
|
|
2021-09-30 03:43:36 +02:00
|
|
|
const Logger = require('../Logger')
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
|
|
|
if (!isNaN(stream.bit_rate) && stream.bit_rate) {
|
|
|
|
return Number(stream.bit_rate)
|
|
|
|
}
|
|
|
|
if (!stream.tags) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to get bitrate from bps tags
|
|
|
|
var bps = stream.tags.BPS || stream.tags['BPS-eng'] || stream.tags['BPS_eng']
|
|
|
|
if (bps && !isNaN(bps)) {
|
|
|
|
return Number(bps)
|
|
|
|
}
|
|
|
|
|
|
|
|
var tagDuration = stream.tags.DURATION || stream.tags['DURATION-eng'] || stream.tags['DURATION_eng']
|
|
|
|
var tagBytes = stream.tags.NUMBER_OF_BYTES || stream.tags['NUMBER_OF_BYTES-eng'] || stream.tags['NUMBER_OF_BYTES_eng']
|
|
|
|
if (tagDuration && tagBytes && !isNaN(tagDuration) && !isNaN(tagBytes)) {
|
|
|
|
var bps = Math.floor(Number(tagBytes) * 8 / Number(tagDuration))
|
|
|
|
if (bps && !isNaN(bps)) {
|
|
|
|
return bps
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (total_bit_rate && stream.codec_type === 'video') {
|
|
|
|
var estimated_bit_rate = total_bit_rate
|
|
|
|
all_streams.forEach((stream) => {
|
|
|
|
if (stream.bit_rate && !isNaN(stream.bit_rate)) {
|
|
|
|
estimated_bit_rate -= Number(stream.bit_rate)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (!all_streams.find(s => s.codec_type === 'audio' && s.bit_rate && Number(s.bit_rate) > estimated_bit_rate)) {
|
|
|
|
return estimated_bit_rate
|
|
|
|
} else {
|
|
|
|
return total_bit_rate
|
|
|
|
}
|
|
|
|
} else if (stream.codec_type === 'audio') {
|
|
|
|
return 112000
|
|
|
|
} else {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function tryGrabFrameRate(stream) {
|
|
|
|
var avgFrameRate = stream.avg_frame_rate || stream.r_frame_rate
|
|
|
|
if (!avgFrameRate) return null
|
|
|
|
var parts = avgFrameRate.split('/')
|
|
|
|
if (parts.length === 2) {
|
|
|
|
avgFrameRate = Number(parts[0]) / Number(parts[1])
|
|
|
|
} else {
|
|
|
|
avgFrameRate = Number(parts[0])
|
|
|
|
}
|
|
|
|
if (!isNaN(avgFrameRate)) return avgFrameRate
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
function tryGrabSampleRate(stream) {
|
|
|
|
var sample_rate = stream.sample_rate
|
|
|
|
if (!isNaN(sample_rate)) return Number(sample_rate)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
function tryGrabChannelLayout(stream) {
|
|
|
|
var layout = stream.channel_layout
|
|
|
|
if (!layout) return null
|
|
|
|
return String(layout).split('(').shift()
|
|
|
|
}
|
|
|
|
|
|
|
|
function tryGrabTag(stream, tag) {
|
|
|
|
if (!stream.tags) return null
|
|
|
|
return stream.tags[tag] || stream.tags[tag.toUpperCase()] || null
|
|
|
|
}
|
|
|
|
|
2021-09-30 03:43:36 +02:00
|
|
|
function tryGrabTags(stream, ...tags) {
|
|
|
|
if (!stream.tags) return null
|
|
|
|
for (let i = 0; i < tags.length; i++) {
|
|
|
|
var value = stream.tags[tags[i]] || stream.tags[tags[i].toUpperCase()]
|
|
|
|
if (value) return value
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
|
|
|
var info = {
|
|
|
|
index: stream.index,
|
|
|
|
type: stream.codec_type,
|
|
|
|
codec: stream.codec_name || null,
|
|
|
|
codec_long: stream.codec_long_name || null,
|
|
|
|
codec_time_base: stream.codec_time_base || null,
|
|
|
|
time_base: stream.time_base || null,
|
|
|
|
bit_rate: tryGrabBitRate(stream, all_streams, total_bit_rate),
|
|
|
|
language: tryGrabTag(stream, 'language'),
|
|
|
|
title: tryGrabTag(stream, 'title')
|
|
|
|
}
|
2021-12-25 01:06:17 +01:00
|
|
|
if (stream.tags) info.tags = stream.tags
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
if (info.type === 'audio' || info.type === 'subtitle') {
|
|
|
|
var disposition = stream.disposition || {}
|
|
|
|
info.is_default = disposition.default === 1 || disposition.default === '1'
|
|
|
|
}
|
|
|
|
|
|
|
|
if (info.type === 'video') {
|
|
|
|
info.profile = stream.profile || null
|
|
|
|
info.is_avc = (stream.is_avc !== '0' && stream.is_avc !== 'false')
|
|
|
|
info.pix_fmt = stream.pix_fmt || null
|
|
|
|
info.frame_rate = tryGrabFrameRate(stream)
|
|
|
|
info.width = !isNaN(stream.width) ? Number(stream.width) : null
|
|
|
|
info.height = !isNaN(stream.height) ? Number(stream.height) : null
|
|
|
|
info.color_range = stream.color_range || null
|
|
|
|
info.color_space = stream.color_space || null
|
|
|
|
info.color_transfer = stream.color_transfer || null
|
|
|
|
info.color_primaries = stream.color_primaries || null
|
|
|
|
} else if (stream.codec_type === 'audio') {
|
|
|
|
info.channels = stream.channels || null
|
|
|
|
info.sample_rate = tryGrabSampleRate(stream)
|
|
|
|
info.channel_layout = tryGrabChannelLayout(stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
return info
|
|
|
|
}
|
|
|
|
|
2022-01-13 23:43:32 +01:00
|
|
|
function isNullOrNaN(val) {
|
|
|
|
return val === null || isNaN(val)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Example chapter object
|
|
|
|
* {
|
|
|
|
"id": 71,
|
|
|
|
"time_base": "1/1000",
|
|
|
|
"start": 80792671,
|
|
|
|
"start_time": "80792.671000",
|
|
|
|
"end": 81084755,
|
|
|
|
"end_time": "81084.755000",
|
|
|
|
"tags": {
|
|
|
|
"title": "072"
|
|
|
|
}
|
|
|
|
* }
|
|
|
|
*/
|
2021-09-08 16:15:54 +02:00
|
|
|
function parseChapters(chapters) {
|
|
|
|
if (!chapters) return []
|
|
|
|
return chapters.map(chap => {
|
2022-01-13 23:43:32 +01:00
|
|
|
var title = chap['TAG:title'] || chap.title || ''
|
|
|
|
if (!title && chap.tags && chap.tags.title) title = chap.tags.title
|
|
|
|
|
2021-09-08 16:15:54 +02:00
|
|
|
var timebase = chap.time_base && chap.time_base.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
2022-01-13 23:43:32 +01:00
|
|
|
var start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
|
|
|
var end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
2021-09-08 16:15:54 +02:00
|
|
|
return {
|
|
|
|
id: chap.id,
|
2022-01-13 23:43:32 +01:00
|
|
|
start,
|
|
|
|
end,
|
2021-09-08 16:15:54 +02:00
|
|
|
title
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-21 01:54:05 +02:00
|
|
|
function parseTags(format, verbose) {
|
2021-09-30 03:43:36 +02:00
|
|
|
if (!format.tags) {
|
|
|
|
return {}
|
|
|
|
}
|
2021-10-21 01:54:05 +02:00
|
|
|
if (verbose) {
|
|
|
|
Logger.debug('Tags', format.tags)
|
|
|
|
}
|
2021-09-30 03:43:36 +02:00
|
|
|
const tags = {
|
|
|
|
file_tag_encoder: tryGrabTags(format, 'encoder', 'tsse', 'tss'),
|
|
|
|
file_tag_encodedby: tryGrabTags(format, 'encoded_by', 'tenc', 'ten'),
|
|
|
|
file_tag_title: tryGrabTags(format, 'title', 'tit2', 'tt2'),
|
|
|
|
file_tag_subtitle: tryGrabTags(format, 'subtitle', 'tit3', 'tt3'),
|
|
|
|
file_tag_track: tryGrabTags(format, 'track', 'trck', 'trk'),
|
2022-01-10 00:36:25 +01:00
|
|
|
file_tag_disc: tryGrabTags(format, 'discnumber', 'disc', 'disk', 'tpos'),
|
2021-09-30 03:43:36 +02:00
|
|
|
file_tag_album: tryGrabTags(format, 'album', 'talb', 'tal'),
|
|
|
|
file_tag_artist: tryGrabTags(format, 'artist', 'tpe1', 'tp1'),
|
2021-11-26 02:23:22 +01:00
|
|
|
file_tag_albumartist: tryGrabTags(format, 'albumartist', 'album_artist', 'tpe2'),
|
2021-09-30 03:43:36 +02:00
|
|
|
file_tag_date: tryGrabTags(format, 'date', 'tyer', 'tye'),
|
|
|
|
file_tag_composer: tryGrabTags(format, 'composer', 'tcom', 'tcm'),
|
|
|
|
file_tag_publisher: tryGrabTags(format, 'publisher', 'tpub', 'tpb'),
|
|
|
|
file_tag_comment: tryGrabTags(format, 'comment', 'comm', 'com'),
|
|
|
|
file_tag_description: tryGrabTags(format, 'description', 'desc'),
|
|
|
|
file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
|
2021-10-24 00:32:42 +02:00
|
|
|
file_tag_series: tryGrabTag(format, 'series'),
|
|
|
|
file_tag_seriespart: tryGrabTag(format, 'series-part'),
|
2022-01-03 20:40:03 +01:00
|
|
|
file_tag_isbn: tryGrabTag(format, 'isbn'),
|
2022-01-10 02:21:13 +01:00
|
|
|
file_tag_language: tryGrabTags(format, 'language', 'lang'),
|
2022-01-10 01:37:16 +01:00
|
|
|
file_tag_asin: tryGrabTag(format, 'asin'),
|
2021-09-30 03:43:36 +02:00
|
|
|
|
|
|
|
// Not sure if these are actually used yet or not
|
|
|
|
file_tag_creation_time: tryGrabTag(format, 'creation_time'),
|
|
|
|
file_tag_wwwaudiofile: tryGrabTags(format, 'wwwaudiofile', 'woaf', 'waf'),
|
|
|
|
file_tag_contentgroup: tryGrabTags(format, 'contentgroup', 'tit1', 'tt1'),
|
|
|
|
file_tag_releasetime: tryGrabTags(format, 'releasetime', 'tdrl'),
|
|
|
|
file_tag_movementname: tryGrabTags(format, 'movementname', 'mvnm'),
|
|
|
|
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
|
|
|
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
2021-10-21 01:54:05 +02:00
|
|
|
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
2021-09-30 03:43:36 +02:00
|
|
|
}
|
|
|
|
for (const key in tags) {
|
|
|
|
if (!tags[key]) {
|
|
|
|
delete tags[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-02 01:33:46 +02:00
|
|
|
// 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]}`)
|
|
|
|
// }
|
|
|
|
// })
|
2021-09-30 03:43:36 +02:00
|
|
|
return tags
|
|
|
|
}
|
|
|
|
|
2021-12-25 01:06:17 +01:00
|
|
|
function getDefaultAudioStream(audioStreams) {
|
|
|
|
if (!audioStreams || !audioStreams.length) return null
|
|
|
|
if (audioStreams.length === 1) return audioStreams[0]
|
|
|
|
var defaultStream = audioStreams.find(a => a.is_default)
|
|
|
|
if (!defaultStream) return audioStreams[0]
|
|
|
|
return defaultStream
|
|
|
|
}
|
|
|
|
|
2021-10-21 01:54:05 +02:00
|
|
|
function parseProbeData(data, verbose = false) {
|
2021-08-18 00:01:11 +02:00
|
|
|
try {
|
2021-09-08 16:15:54 +02:00
|
|
|
var { format, streams, chapters } = data
|
2021-08-18 00:01:11 +02:00
|
|
|
var { format_long_name, duration, size, bit_rate } = format
|
|
|
|
|
|
|
|
var sizeBytes = !isNaN(size) ? Number(size) : null
|
|
|
|
var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
|
2021-09-30 03:43:36 +02:00
|
|
|
|
|
|
|
// Logger.debug('Parsing Data for', Path.basename(format.filename))
|
2021-10-21 01:54:05 +02:00
|
|
|
var tags = parseTags(format, verbose)
|
2021-08-18 00:01:11 +02:00
|
|
|
var cleanedData = {
|
|
|
|
format: format_long_name,
|
|
|
|
duration: !isNaN(duration) ? Number(duration) : null,
|
|
|
|
size: sizeBytes,
|
|
|
|
sizeMb,
|
|
|
|
bit_rate: !isNaN(bit_rate) ? Number(bit_rate) : null,
|
2021-09-30 03:43:36 +02:00
|
|
|
...tags
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2021-10-21 01:54:05 +02:00
|
|
|
if (verbose && format.tags) {
|
|
|
|
cleanedData.rawTags = format.tags
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
|
|
|
cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video')
|
2021-12-25 01:06:17 +01:00
|
|
|
var audioStreams = cleaned_streams.filter(s => s.type === 'audio')
|
|
|
|
cleanedData.audio_stream = getDefaultAudioStream(audioStreams)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-12-25 01:06:17 +01:00
|
|
|
if (cleanedData.audio_stream && cleanedData.video_stream) {
|
2021-08-18 00:01:11 +02:00
|
|
|
var videoBitrate = cleanedData.video_stream.bit_rate
|
|
|
|
// If audio stream bitrate larger then video, most likely incorrect
|
2021-12-25 01:06:17 +01:00
|
|
|
if (cleanedData.audio_stream.bit_rate > videoBitrate) {
|
2021-08-18 00:01:11 +02:00
|
|
|
cleanedData.video_stream.bit_rate = cleanedData.bit_rate
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-25 01:06:17 +01:00
|
|
|
// If format does not have tags, check audio stream (https://github.com/advplyr/audiobookshelf/issues/256)
|
|
|
|
if (!format.tags && cleanedData.audio_stream && cleanedData.audio_stream.tags) {
|
|
|
|
var tags = parseTags(cleanedData.audio_stream)
|
|
|
|
cleanedData = {
|
|
|
|
...cleanedData,
|
|
|
|
...tags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-08 16:15:54 +02:00
|
|
|
cleanedData.chapters = parseChapters(chapters)
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
return cleanedData
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Parse failed', error)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-23 02:58:20 +01:00
|
|
|
// Updated probe returns AudioProbeData object
|
2021-12-25 01:06:17 +01:00
|
|
|
function probe(filepath, verbose = false) {
|
2022-02-13 22:00:59 +01:00
|
|
|
if (process.env.FFPROBE_PATH) {
|
|
|
|
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
|
|
|
}
|
|
|
|
|
2022-02-07 00:14:55 +01:00
|
|
|
return ffprobe(filepath)
|
2022-01-07 14:15:53 +01:00
|
|
|
.then(raw => {
|
|
|
|
var rawProbeData = parseProbeData(raw, verbose)
|
|
|
|
if (!rawProbeData || !rawProbeData.audio_stream) {
|
|
|
|
return {
|
|
|
|
error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed'
|
2021-11-23 02:58:20 +01:00
|
|
|
}
|
2022-01-07 14:15:53 +01:00
|
|
|
} else {
|
|
|
|
var probeData = new AudioProbeData()
|
|
|
|
probeData.setData(rawProbeData)
|
|
|
|
return probeData
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
return {
|
|
|
|
error: err
|
2021-11-23 02:58:20 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2021-12-25 01:06:17 +01:00
|
|
|
module.exports.probe = probe
|