mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-24 04:39:40 +00:00
Change:Fallback to audio stream tags if probe format has no tags and remove old scanner #256
This commit is contained in:
parent
3f8551f9a1
commit
a17348f916
12 changed files with 97 additions and 1269 deletions
|
|
@ -1,317 +0,0 @@
|
|||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const prober = require('./prober')
|
||||
|
||||
const ImageCodecs = ['mjpeg', 'jpeg', 'png']
|
||||
|
||||
function getDefaultAudioStream(audioStreams) {
|
||||
if (audioStreams.length === 1) return audioStreams[0]
|
||||
var defaultStream = audioStreams.find(a => a.is_default)
|
||||
if (!defaultStream) return audioStreams[0]
|
||||
return defaultStream
|
||||
}
|
||||
|
||||
async function scan(path, verbose = false) {
|
||||
Logger.debug(`Scanning path "${path}"`)
|
||||
var probeData = await prober.probe(path, verbose)
|
||||
if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
|
||||
return {
|
||||
error: 'Invalid audio file'
|
||||
}
|
||||
}
|
||||
if (!probeData.duration || !probeData.size) {
|
||||
return {
|
||||
error: 'Invalid duration or size'
|
||||
}
|
||||
}
|
||||
var audioStream = getDefaultAudioStream(probeData.audio_streams)
|
||||
|
||||
const finalData = {
|
||||
format: probeData.format,
|
||||
duration: probeData.duration,
|
||||
size: probeData.size,
|
||||
bit_rate: audioStream.bit_rate || probeData.bit_rate,
|
||||
codec: audioStream.codec,
|
||||
time_base: audioStream.time_base,
|
||||
language: audioStream.language,
|
||||
channel_layout: audioStream.channel_layout,
|
||||
channels: audioStream.channels,
|
||||
sample_rate: audioStream.sample_rate,
|
||||
chapters: probeData.chapters || []
|
||||
}
|
||||
|
||||
var hasCoverArt = probeData.video_stream ? ImageCodecs.includes(probeData.video_stream.codec) : false
|
||||
if (hasCoverArt) {
|
||||
finalData.embedded_cover_art = probeData.video_stream.codec
|
||||
}
|
||||
|
||||
for (const key in probeData) {
|
||||
if (probeData[key] && key.startsWith('file_tag')) {
|
||||
finalData[key] = probeData[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (finalData.file_tag_track) {
|
||||
var track = finalData.file_tag_track
|
||||
var trackParts = track.split('/').map(part => Number(part))
|
||||
if (trackParts.length > 0) {
|
||||
finalData.trackNumber = trackParts[0]
|
||||
}
|
||||
if (trackParts.length > 1) {
|
||||
finalData.trackTotal = trackParts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose && probeData.rawTags) {
|
||||
finalData.rawTags = probeData.rawTags
|
||||
}
|
||||
|
||||
return finalData
|
||||
}
|
||||
module.exports.scan = scan
|
||||
|
||||
|
||||
function isNumber(val) {
|
||||
return !isNaN(val) && val !== null
|
||||
}
|
||||
|
||||
function getTrackNumberFromMeta(scanData) {
|
||||
return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Math.trunc(Number(scanData.trackNumber)) : null
|
||||
}
|
||||
|
||||
function getTrackNumberFromFilename(title, author, series, publishYear, filename) {
|
||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||
|
||||
// Remove title, author, series, and publishYear from filename if there
|
||||
if (title) partbasename = partbasename.replace(title, '')
|
||||
if (author) partbasename = partbasename.replace(author, '')
|
||||
if (series) partbasename = partbasename.replace(series, '')
|
||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||
|
||||
// Remove eg. "disc 1" from path
|
||||
partbasename = partbasename.replace(/\bdisc \d\d?\b/i, '')
|
||||
|
||||
// Remove "cd01" or "cd 01" from path
|
||||
partbasename = partbasename.replace(/\bcd ?\d\d?\b/i, '')
|
||||
|
||||
var numbersinpath = partbasename.match(/\d{1,4}/g)
|
||||
if (!numbersinpath) return null
|
||||
|
||||
var number = numbersinpath.length ? parseInt(numbersinpath[0]) : null
|
||||
return number
|
||||
}
|
||||
|
||||
function getCdNumberFromFilename(title, author, series, publishYear, filename) {
|
||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||
|
||||
// Remove title, author, series, and publishYear from filename if there
|
||||
if (title) partbasename = partbasename.replace(title, '')
|
||||
if (author) partbasename = partbasename.replace(author, '')
|
||||
if (series) partbasename = partbasename.replace(series, '')
|
||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||
|
||||
var cdNumber = null
|
||||
|
||||
var cdmatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i)
|
||||
if (cdmatch && cdmatch.length > 2 && cdmatch[2]) {
|
||||
if (!isNaN(cdmatch[2])) {
|
||||
cdNumber = Number(cdmatch[2])
|
||||
}
|
||||
}
|
||||
|
||||
return cdNumber
|
||||
}
|
||||
|
||||
async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
if (!newAudioFiles || !newAudioFiles.length) {
|
||||
Logger.error('[AudioFileScanner] Scan Audio Files no new files', audiobook.title)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.debug('[AudioFileScanner] Scanning audio files')
|
||||
|
||||
var tracks = []
|
||||
var numDuplicateTracks = 0
|
||||
var numInvalidTracks = 0
|
||||
|
||||
for (let i = 0; i < newAudioFiles.length; i++) {
|
||||
var audioFile = newAudioFiles[i]
|
||||
var scanData = await scan(audioFile.fullPath)
|
||||
if (!scanData || scanData.error) {
|
||||
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
||||
continue;
|
||||
}
|
||||
|
||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||
var book = audiobook.book || {}
|
||||
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
// IF CD num was found but no track num - USE cd num as track num
|
||||
if (!trackNumFromFilename && cdNumFromFilename) {
|
||||
trackNumFromFilename = cdNumFromFilename
|
||||
cdNumFromFilename = null
|
||||
}
|
||||
|
||||
var audioFileObj = {
|
||||
ino: audioFile.ino,
|
||||
filename: audioFile.filename,
|
||||
path: audioFile.path,
|
||||
fullPath: audioFile.fullPath,
|
||||
ext: audioFile.ext,
|
||||
...scanData,
|
||||
trackNumFromMeta,
|
||||
trackNumFromFilename,
|
||||
cdNumFromFilename
|
||||
}
|
||||
var audioFile = audiobook.addAudioFile(audioFileObj)
|
||||
|
||||
var trackNumber = 1
|
||||
if (newAudioFiles.length > 1) {
|
||||
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
||||
if (trackNumber === null) {
|
||||
Logger.debug('[AudioFileScanner] Invalid track number for', audioFile.filename)
|
||||
audioFile.invalid = true
|
||||
audioFile.error = 'Failed to get track number'
|
||||
numInvalidTracks++
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (tracks.find(t => t.index === trackNumber)) {
|
||||
// Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
||||
audioFile.invalid = true
|
||||
audioFile.error = 'Duplicate track number'
|
||||
numDuplicateTracks++
|
||||
continue;
|
||||
}
|
||||
|
||||
audioFile.index = trackNumber
|
||||
tracks.push(audioFile)
|
||||
}
|
||||
|
||||
if (!tracks.length) {
|
||||
Logger.warn('[AudioFileScanner] No Tracks for audiobook', audiobook.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (numDuplicateTracks > 0) {
|
||||
Logger.warn(`[AudioFileScanner] ${numDuplicateTracks} Duplicate tracks for "${audiobook.title}"`)
|
||||
}
|
||||
if (numInvalidTracks > 0) {
|
||||
Logger.error(`[AudioFileScanner] ${numDuplicateTracks} Invalid tracks for "${audiobook.title}"`)
|
||||
}
|
||||
|
||||
tracks.sort((a, b) => a.index - b.index)
|
||||
|
||||
audiobook.audioFiles.sort((a, b) => {
|
||||
var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0
|
||||
var bNum = isNumber(b.trackNumFromMeta) ? b.trackNumFromMeta : isNumber(b.trackNumFromFilename) ? b.trackNumFromFilename : 0
|
||||
return aNum - bNum
|
||||
})
|
||||
|
||||
// If first index is 0, increment all by 1
|
||||
if (tracks[0].index === 0) {
|
||||
tracks = tracks.map(t => {
|
||||
t.index += 1
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
var hasTracksAlready = audiobook.tracks.length
|
||||
tracks.forEach((track) => {
|
||||
audiobook.addTrack(track)
|
||||
})
|
||||
if (hasTracksAlready) {
|
||||
audiobook.tracks.sort((a, b) => a.index - b.index)
|
||||
}
|
||||
}
|
||||
module.exports.scanAudioFiles = scanAudioFiles
|
||||
|
||||
|
||||
async function rescanAudioFiles(audiobook) {
|
||||
var audioFiles = audiobook.audioFiles
|
||||
var updates = 0
|
||||
|
||||
for (let i = 0; i < audioFiles.length; i++) {
|
||||
var audioFile = audioFiles[i]
|
||||
var scanData = await scan(audioFile.fullPath)
|
||||
if (!scanData || scanData.error) {
|
||||
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
||||
// audiobook.invalidAudioFiles.push(parts[i])
|
||||
continue;
|
||||
}
|
||||
|
||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||
var book = audiobook.book || {}
|
||||
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
|
||||
|
||||
// IF CD num was found but no track num - USE cd num as track num
|
||||
if (!trackNumFromFilename && cdNumFromFilename) {
|
||||
trackNumFromFilename = cdNumFromFilename
|
||||
cdNumFromFilename = null
|
||||
}
|
||||
|
||||
var metadataUpdate = {
|
||||
...scanData,
|
||||
trackNumFromMeta,
|
||||
trackNumFromFilename,
|
||||
cdNumFromFilename
|
||||
}
|
||||
var hasUpdates = audioFile.updateMetadata(metadataUpdate)
|
||||
if (hasUpdates) {
|
||||
// Sync audio track with audio file
|
||||
var matchingAudioTrack = audiobook.tracks.find(t => t.ino === audioFile.ino)
|
||||
if (matchingAudioTrack) {
|
||||
matchingAudioTrack.syncMetadata(audioFile)
|
||||
} else if (!audioFile.exclude) { // If audio file is not excluded then it should have an audio track
|
||||
|
||||
// Fallback to checking path
|
||||
matchingAudioTrack = audiobook.tracks.find(t => t.path === audioFile.path)
|
||||
if (matchingAudioTrack) {
|
||||
Logger.error(`[AudioFileScanner] Audio File mismatch ino with audio track "${audioFile.filename}"`)
|
||||
matchingAudioTrack.ino = audioFile.ino
|
||||
matchingAudioTrack.syncMetadata(audioFile)
|
||||
} else {
|
||||
Logger.error(`[AudioFileScanner] Audio File has no matching Track ${audioFile.filename} for "${audiobook.title}"`)
|
||||
|
||||
// Exclude audio file to prevent further errors
|
||||
// audioFile.exclude = true
|
||||
}
|
||||
}
|
||||
updates++
|
||||
}
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
module.exports.rescanAudioFiles = rescanAudioFiles
|
||||
|
||||
async function scanTrackNumbers(audiobook) {
|
||||
var tracks = audiobook.tracks || []
|
||||
var scannedTrackNumData = []
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
var track = tracks[i]
|
||||
var scanData = await scan(track.fullPath, true)
|
||||
|
||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||
var book = audiobook.book || {}
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, track.filename)
|
||||
Logger.info(`[AudioFileScanner] Track # for "${track.filename}", Metadata: "${trackNumFromMeta}", Filename: "${trackNumFromFilename}", Current: "${track.index}"`)
|
||||
scannedTrackNumData.push({
|
||||
filename: track.filename,
|
||||
currentTrackNum: track.index,
|
||||
trackNumFromFilename,
|
||||
trackNumFromMeta,
|
||||
scanDataTrackNum: scanData.file_tag_track,
|
||||
rawTags: scanData.rawTags || null
|
||||
})
|
||||
}
|
||||
return scannedTrackNumData
|
||||
}
|
||||
module.exports.scanTrackNumbers = scanTrackNumbers
|
||||
|
|
@ -98,6 +98,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
|||
language: tryGrabTag(stream, 'language'),
|
||||
title: tryGrabTag(stream, 'title')
|
||||
}
|
||||
if (stream.tags) info.tags = stream.tags
|
||||
|
||||
if (info.type === 'audio' || info.type === 'subtitle') {
|
||||
var disposition = stream.disposition || {}
|
||||
|
|
@ -188,6 +189,14 @@ function parseTags(format, verbose) {
|
|||
return tags
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function parseProbeData(data, verbose = false) {
|
||||
try {
|
||||
var { format, streams, chapters } = data
|
||||
|
|
@ -212,17 +221,26 @@ function parseProbeData(data, verbose = false) {
|
|||
|
||||
const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
||||
cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video')
|
||||
cleanedData.audio_streams = cleaned_streams.filter(s => s.type === 'audio')
|
||||
cleanedData.subtitle_streams = cleaned_streams.filter(s => s.type === 'subtitle')
|
||||
var audioStreams = cleaned_streams.filter(s => s.type === 'audio')
|
||||
cleanedData.audio_stream = getDefaultAudioStream(audioStreams)
|
||||
|
||||
if (cleanedData.audio_streams.length && cleanedData.video_stream) {
|
||||
if (cleanedData.audio_stream && cleanedData.video_stream) {
|
||||
var videoBitrate = cleanedData.video_stream.bit_rate
|
||||
// If audio stream bitrate larger then video, most likely incorrect
|
||||
if (cleanedData.audio_streams.find(astream => astream.bit_rate > videoBitrate)) {
|
||||
if (cleanedData.audio_stream.bit_rate > videoBitrate) {
|
||||
cleanedData.video_stream.bit_rate = cleanedData.bit_rate
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
cleanedData.chapters = parseChapters(chapters)
|
||||
|
||||
return cleanedData
|
||||
|
|
@ -232,22 +250,8 @@ function parseProbeData(data, verbose = false) {
|
|||
}
|
||||
}
|
||||
|
||||
function probe(filepath, verbose = false) {
|
||||
return new Promise((resolve) => {
|
||||
Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve(parseProbeData(raw, verbose))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
module.exports.probe = probe
|
||||
|
||||
// Updated probe returns AudioProbeData object
|
||||
function probe2(filepath, verbose = false) {
|
||||
function probe(filepath, verbose = false) {
|
||||
return new Promise((resolve) => {
|
||||
Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => {
|
||||
if (err) {
|
||||
|
|
@ -258,7 +262,7 @@ function probe2(filepath, verbose = false) {
|
|||
})
|
||||
} else {
|
||||
var rawProbeData = parseProbeData(raw, verbose)
|
||||
if (!rawProbeData || !rawProbeData.audio_streams.length) {
|
||||
if (!rawProbeData || !rawProbeData.audio_stream) {
|
||||
resolve({
|
||||
error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed'
|
||||
})
|
||||
|
|
@ -271,4 +275,4 @@ function probe2(filepath, verbose = false) {
|
|||
})
|
||||
})
|
||||
}
|
||||
module.exports.probe2 = probe2
|
||||
module.exports.probe = probe
|
||||
Loading…
Add table
Add a link
Reference in a new issue