diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index 10437bc1..3f126889 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -8,11 +8,11 @@ const LibraryItem = require('../models/LibraryItem') const AudioFile = require('../objects/files/AudioFile') class AudioFileScanner { - constructor() { } + constructor() {} /** * Is array of numbers sequential, i.e. 1, 2, 3, 4 - * @param {number[]} nums + * @param {number[]} nums * @returns {boolean} */ isSequential(nums) { @@ -27,8 +27,8 @@ class AudioFileScanner { } /** - * Remove - * @param {number[]} nums + * Remove + * @param {number[]} nums * @returns {number[]} */ removeDupes(nums) { @@ -44,8 +44,8 @@ class AudioFileScanner { /** * Order audio files by track/disc number - * @param {string} libraryItemRelPath - * @param {import('../models/Book').AudioFileObject[]} audioFiles + * @param {string} libraryItemRelPath + * @param {import('../models/Book').AudioFileObject[]} audioFiles * @returns {import('../models/Book').AudioFileObject[]} */ runSmartTrackOrder(libraryItemRelPath, audioFiles) { @@ -103,8 +103,8 @@ class AudioFileScanner { /** * Get track and disc number from audio filename - * @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan - * @param {LibraryItem.LibraryFileObject} audioLibraryFile + * @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan + * @param {LibraryItem.LibraryFileObject} audioLibraryFile * @returns {{trackNumber:number, discNumber:number}} */ getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) { @@ -146,10 +146,10 @@ class AudioFileScanner { } /** - * - * @param {string} mediaType - * @param {LibraryItem.LibraryFileObject} libraryFile - * @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan + * + * @param {string} mediaType + * @param {LibraryItem.LibraryFileObject} libraryFile + * @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan * @returns {Promise} */ async scan(mediaType, libraryFile, mediaMetadataFromScan) { @@ -181,7 +181,7 @@ class AudioFileScanner { /** * Scan LibraryFiles and return AudioFiles * @param {string} mediaType - * @param {import('./LibraryItemScanData')} libraryItemScanData + * @param {import('./LibraryItemScanData')} libraryItemScanData * @param {LibraryItem.LibraryFileObject[]} audioLibraryFiles * @returns {Promise} */ @@ -193,15 +193,15 @@ class AudioFileScanner { for (let i = batch; i < Math.min(batch + batchSize, audioLibraryFiles.length); i++) { proms.push(this.scan(mediaType, audioLibraryFiles[i], libraryItemScanData.mediaMetadata)) } - results.push(...await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))) + results.push(...(await Promise.all(proms).then((scanResults) => scanResults.filter((sr) => sr)))) } return results } /** - * - * @param {AudioFile} audioFile + * + * @param {AudioFile} audioFile * @returns {object} */ probeAudioFile(audioFile) { @@ -211,10 +211,10 @@ class AudioFileScanner { /** * Set book metadata & chapters from audio file meta tags - * + * * @param {string} bookTitle - * @param {import('../models/Book').AudioFileObject} audioFile - * @param {Object} bookMetadata + * @param {import('../models/Book').AudioFileObject} audioFile + * @param {Object} bookMetadata * @param {import('./LibraryScan')} libraryScan */ setBookMetadataFromAudioMetaTags(bookTitle, audioFiles, bookMetadata, libraryScan) { @@ -243,7 +243,7 @@ class AudioFileScanner { { tag: 'tagAlbum', altTag: 'tagTitle', - key: 'title', + key: 'title' }, { tag: 'tagArtist', @@ -311,9 +311,9 @@ class AudioFileScanner { /** * Set podcast metadata from first audio file - * - * @param {import('../models/Book').AudioFileObject} audioFile - * @param {Object} podcastMetadata + * + * @param {import('../models/Book').AudioFileObject} audioFile + * @param {Object} podcastMetadata * @param {import('./LibraryScan')} libraryScan */ setPodcastMetadataFromAudioMetaTags(audioFile, podcastMetadata, libraryScan) { @@ -343,7 +343,7 @@ class AudioFileScanner { }, { tag: 'tagPodcastType', - key: 'podcastType', + key: 'podcastType' } ] @@ -370,7 +370,7 @@ class AudioFileScanner { } /** - * + * * @param {import('../models/PodcastEpisode')} podcastEpisode Not the model when creating new podcast * @param {import('./ScanLogger')} scanLogger */ @@ -391,7 +391,7 @@ class AudioFileScanner { }, { tag: 'tagDisc', - key: 'season', + key: 'season' }, { tag: 'tagTrack', @@ -446,7 +446,7 @@ class AudioFileScanner { /** * @param {string} bookTitle - * @param {AudioFile[]} audioFiles + * @param {AudioFile[]} audioFiles * @param {import('./LibraryScan')} libraryScan * @returns {import('../models/Book').ChapterObject[]} */ @@ -464,12 +464,7 @@ class AudioFileScanner { // If first audio file has embedded chapters then use embedded chapters if (audioFiles[0].chapters?.length) { // If all files chapters are the same, then only make chapters for the first file - if ( - audioFiles.length === 1 || - audioFiles.length > 1 && - audioFiles[0].chapters.length === audioFiles[1].chapters?.length && - audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start) - ) { + if (audioFiles.length === 1 || (audioFiles.length > 1 && audioFiles[0].chapters.length === audioFiles[1].chapters?.length && audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start))) { libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`) chapters = audioFiles[0].chapters.map((c) => ({ ...c })) } else { @@ -479,12 +474,13 @@ class AudioFileScanner { audioFiles.forEach((file) => { if (file.duration) { - const afChapters = file.chapters?.map((c) => ({ - ...c, - id: c.id + currChapterId, - start: c.start + currStartTime, - end: c.end + currStartTime, - })) ?? [] + const afChapters = + file.chapters?.map((c) => ({ + ...c, + id: c.id + currChapterId, + start: c.start + currStartTime, + end: c.end + currStartTime + })) ?? [] chapters = chapters.concat(afChapters) currChapterId += file.chapters?.length ?? 0 @@ -494,12 +490,11 @@ class AudioFileScanner { return chapters } } else if (audioFiles.length > 1) { - // In some cases the ID3 title tag for each file is the chapter title, the criteria to determine if this will be used // 1. Every audio file has an ID3 title tag set // 2. None of the title tags are the same as the book title // 3. Every ID3 title tag is unique - const metaTagTitlesFound = [...new Set(audioFiles.map(af => af.metaTags?.tagTitle).filter(tagTitle => !!tagTitle && tagTitle !== bookTitle))] + const metaTagTitlesFound = [...new Set(audioFiles.map((af) => af.metaTags?.tagTitle).filter((tagTitle) => !!tagTitle && tagTitle !== bookTitle))] const useMetaTagAsTitle = metaTagTitlesFound.length === audioFiles.length // Build chapters from audio files @@ -528,8 +523,8 @@ class AudioFileScanner { /** * Parse a genre string into multiple genres * @example "Fantasy;Sci-Fi;History" => ["Fantasy", "Sci-Fi", "History"] - * - * @param {string} genreTag + * + * @param {string} genreTag * @returns {string[]} */ parseGenresString(genreTag) { @@ -537,10 +532,13 @@ class AudioFileScanner { const separators = ['/', '//', ';'] for (let i = 0; i < separators.length; i++) { if (genreTag.includes(separators[i])) { - return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) + return genreTag + .split(separators[i]) + .map((genre) => genre.trim()) + .filter((g) => !!g) } } return [genreTag] } } -module.exports = new AudioFileScanner() \ No newline at end of file +module.exports = new AudioFileScanner() diff --git a/server/utils/prober.js b/server/utils/prober.js index 8e293053..fc58937b 100644 --- a/server/utils/prober.js +++ b/server/utils/prober.js @@ -20,7 +20,7 @@ function tryGrabBitRate(stream, all_streams, total_bit_rate) { 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)) + var bps = Math.floor((Number(tagBytes) * 8) / Number(tagDuration)) if (bps && !isNaN(bps)) { return bps } @@ -33,7 +33,7 @@ function tryGrabBitRate(stream, all_streams, total_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)) { + 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 @@ -73,7 +73,7 @@ function tryGrabChannelLayout(stream) { function tryGrabTags(stream, ...tags) { if (!stream.tags) return null for (let i = 0; i < tags.length; i++) { - const tagKey = Object.keys(stream.tags).find(t => t.toLowerCase() === tags[i].toLowerCase()) + const tagKey = Object.keys(stream.tags).find((t) => t.toLowerCase() === tags[i].toLowerCase()) const value = stream.tags[tagKey] if (value && value.trim()) return value.trim() } @@ -101,7 +101,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) { if (info.type === 'video') { info.profile = stream.profile || null - info.is_avc = (stream.is_avc !== '0' && stream.is_avc !== 'false') + 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 @@ -123,7 +123,6 @@ function isNullOrNaN(val) { return val === null || isNaN(val) } - /* Example chapter object * { "id": 71, @@ -137,23 +136,28 @@ function isNullOrNaN(val) { } * } */ -function parseChapters(chapters) { - if (!chapters) return [] - let index = 0 - return chapters.map(chap => { - let title = chap['TAG:title'] || chap.title || '' - if (!title && chap.tags?.title) title = chap.tags.title +function parseChapters(_chapters) { + if (!_chapters) return [] - const timebase = chap.time_base?.includes('/') ? Number(chap.time_base.split('/')[1]) : 1 - const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0 - const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0 - return { - id: index++, - start, - end, - title - } - }) + return _chapters + .map((chap) => { + let title = chap['TAG:title'] || chap.title || '' + if (!title && chap.tags?.title) title = chap.tags.title + + const timebase = chap.time_base?.includes('/') ? Number(chap.time_base.split('/')[1]) : 1 + const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0 + const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0 + return { + start, + end, + title + } + }) + .toSorted((a, b) => a.start - b.start) + .map((chap, index) => { + chap.id = index + return chap + }) } function parseTags(format, verbose) { @@ -210,7 +214,7 @@ function parseTags(format, verbose) { file_tag_movement: tryGrabTags(format, 'movement', 'mvin'), file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'), file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'), - file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers'), + file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers') } for (const key in tags) { if (!tags[key]) { @@ -224,7 +228,7 @@ function parseTags(format, verbose) { function getDefaultAudioStream(audioStreams) { if (!audioStreams || !audioStreams.length) return null if (audioStreams.length === 1) return audioStreams[0] - var defaultStream = audioStreams.find(a => a.is_default) + var defaultStream = audioStreams.find((a) => a.is_default) if (!defaultStream) return audioStreams[0] return defaultStream } @@ -248,9 +252,9 @@ function parseProbeData(data, verbose = false) { cleanedData.rawTags = format.tags } - const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate)) - cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video') - const audioStreams = cleaned_streams.filter(s => s.type === 'audio') + const cleaned_streams = streams.map((s) => parseMediaStreamInfo(s, streams, cleanedData.bit_rate)) + cleanedData.video_stream = cleaned_streams.find((s) => s.type === 'video') + const audioStreams = cleaned_streams.filter((s) => s.type === 'audio') cleanedData.audio_stream = getDefaultAudioStream(audioStreams) if (cleanedData.audio_stream && cleanedData.video_stream) { @@ -280,8 +284,8 @@ function parseProbeData(data, verbose = false) { /** * Run ffprobe on audio filepath - * @param {string} filepath - * @param {boolean} [verbose=false] + * @param {string} filepath + * @param {boolean} [verbose=false] * @returns {import('../scanner/MediaProbeData')|{error:string}} */ function probe(filepath, verbose = false) { @@ -290,7 +294,7 @@ function probe(filepath, verbose = false) { } return ffprobe(filepath) - .then(raw => { + .then((raw) => { if (raw.error) { return { error: raw.error.string @@ -318,7 +322,7 @@ module.exports.probe = probe /** * Ffprobe for audio file path - * + * * @param {string} filepath * @returns {Object} ffprobe json output */ @@ -327,11 +331,10 @@ function rawProbe(filepath) { ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH } - return ffprobe(filepath) - .catch((err) => { - return { - error: err - } - }) + return ffprobe(filepath).catch((err) => { + return { + error: err + } + }) } -module.exports.rawProbe = rawProbe \ No newline at end of file +module.exports.rawProbe = rawProbe