mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-03-01 08:31:18 +01:00
Scanner v4, audio file metadata used in setting book details, embedded cover art extracted and used
This commit is contained in:
parent
b74b12301c
commit
b26c1ba886
@ -94,13 +94,17 @@ export default {
|
|||||||
return audiobooks.slice(0, 10)
|
return audiobooks.slice(0, 10)
|
||||||
},
|
},
|
||||||
shelves() {
|
shelves() {
|
||||||
var shelves = [
|
var shelves = []
|
||||||
{ books: this.mostRecentPlayed, label: 'Continue Reading' },
|
if (this.mostRecentPlayed.length) {
|
||||||
{ books: this.mostRecentAdded, label: 'Recently Added' }
|
shelves.push({ books: this.mostRecentPlayed, label: 'Continue Reading' })
|
||||||
]
|
}
|
||||||
|
|
||||||
|
shelves.push({ books: this.mostRecentAdded, label: 'Recently Added' })
|
||||||
|
|
||||||
if (this.recentlyUpdatedSeries) {
|
if (this.recentlyUpdatedSeries) {
|
||||||
shelves.push({ books: this.recentlyUpdatedSeries, label: 'Newest Series' })
|
shelves.push({ books: this.recentlyUpdatedSeries, label: 'Newest Series' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.booksRecentlyRead.length) {
|
if (this.booksRecentlyRead.length) {
|
||||||
shelves.push({ books: this.booksRecentlyRead, label: 'Read Again' })
|
shelves.push({ books: this.booksRecentlyRead, label: 'Read Again' })
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -179,7 +179,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('failed to reset audiobooks', error)
|
console.error('failed to reset audiobooks', error)
|
||||||
this.isResettingAudiobooks = false
|
this.isResettingAudiobooks = false
|
||||||
this.$toast.error('Failed to reset audiobooks - stop docker and manually remove appdata')
|
this.$toast.error('Failed to reset audiobooks - manually remove the /config/audiobooks folder')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -37,7 +37,6 @@ class ApiController {
|
|||||||
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
|
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
|
||||||
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
|
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
|
||||||
|
|
||||||
this.router.get('/metadata/:id/:trackIndex', this.getMetadata.bind(this))
|
|
||||||
this.router.patch('/match/:id', this.match.bind(this))
|
this.router.patch('/match/:id', this.match.bind(this))
|
||||||
|
|
||||||
this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))
|
this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))
|
||||||
@ -70,11 +69,6 @@ class ApiController {
|
|||||||
this.scanner.findCovers(req, res)
|
this.scanner.findCovers(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(req, res) {
|
|
||||||
var metadata = await this.scanner.fetchMetadata(req.params.id, req.params.trackIndex)
|
|
||||||
res.json(metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
authorize(req, res) {
|
authorize(req, res) {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
Logger.error('Invalid user in authorize')
|
Logger.error('Invalid user in authorize')
|
||||||
|
@ -7,7 +7,7 @@ const audioFileScanner = require('./utils/audioFileScanner')
|
|||||||
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('./utils/scandir')
|
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('./utils/scandir')
|
||||||
const { comparePaths, getIno } = require('./utils/index')
|
const { comparePaths, getIno } = require('./utils/index')
|
||||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||||
const { ScanResult } = require('./utils/constants')
|
const { ScanResult, CoverDestination } = require('./utils/constants')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
||||||
@ -27,6 +27,20 @@ class Scanner {
|
|||||||
return this.db.audiobooks
|
return this.db.audiobooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCoverDirectory(audiobook) {
|
||||||
|
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
|
||||||
|
return {
|
||||||
|
fullPath: audiobook.fullPath,
|
||||||
|
relPath: Path.join('/local', audiobook.path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
fullPath: Path.join(this.BookMetadataPath, audiobook.id),
|
||||||
|
relPath: Path.join('/metadata', 'books', audiobook.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async setAudioFileInos(audiobookDataAudioFiles, audiobookAudioFiles) {
|
async setAudioFileInos(audiobookDataAudioFiles, audiobookAudioFiles) {
|
||||||
for (let i = 0; i < audiobookDataAudioFiles.length; i++) {
|
for (let i = 0; i < audiobookDataAudioFiles.length; i++) {
|
||||||
var abdFile = audiobookDataAudioFiles[i]
|
var abdFile = audiobookDataAudioFiles[i]
|
||||||
@ -48,7 +62,7 @@ class Scanner {
|
|||||||
|
|
||||||
async scanAudiobookData(audiobookData) {
|
async scanAudiobookData(audiobookData) {
|
||||||
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
||||||
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
// Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
||||||
|
|
||||||
if (existingAudiobook) {
|
if (existingAudiobook) {
|
||||||
// REMOVE: No valid audio files
|
// REMOVE: No valid audio files
|
||||||
@ -64,8 +78,6 @@ class Scanner {
|
|||||||
|
|
||||||
// ino is now set for every file in scandir
|
// ino is now set for every file in scandir
|
||||||
audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino)
|
audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino)
|
||||||
// audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles)
|
|
||||||
|
|
||||||
|
|
||||||
// Check for audio files that were removed
|
// Check for audio files that were removed
|
||||||
var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino)
|
var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino)
|
||||||
@ -124,7 +136,8 @@ class Scanner {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) {
|
var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles)
|
||||||
|
if (otherFilesUpdated) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +180,19 @@ class Scanner {
|
|||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (audiobook.hasDescriptionTextFile) {
|
||||||
|
await audiobook.saveDescriptionFromTextFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audiobook.hasEmbeddedCoverArt) {
|
||||||
|
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
||||||
|
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
||||||
|
if (relativeDir) {
|
||||||
|
Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audiobook.setDetailsFromFileMetadata()
|
||||||
audiobook.checkUpdateMissingParts()
|
audiobook.checkUpdateMissingParts()
|
||||||
audiobook.setChapters()
|
audiobook.setChapters()
|
||||||
|
|
||||||
@ -177,14 +203,11 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scan() {
|
async scan() {
|
||||||
// TODO: This temporary fix from pre-release should be removed soon, including the "fixRelativePath" and "checkUpdateInos"
|
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
|
||||||
// TEMP - fix relative file paths
|
|
||||||
// TEMP - update ino for each audiobook
|
// TEMP - update ino for each audiobook
|
||||||
if (this.audiobooks.length) {
|
if (this.audiobooks.length) {
|
||||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||||
var ab = this.audiobooks[i]
|
var ab = this.audiobooks[i]
|
||||||
// var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
|
|
||||||
|
|
||||||
// Update ino if inos are not set
|
// Update ino if inos are not set
|
||||||
var shouldUpdateIno = ab.hasMissingIno
|
var shouldUpdateIno = ab.hasMissingIno
|
||||||
if (shouldUpdateIno) {
|
if (shouldUpdateIno) {
|
||||||
@ -319,10 +342,6 @@ class Scanner {
|
|||||||
var relfilepaths = filepaths.map(path => path.replace(this.AudiobookPath, ''))
|
var relfilepaths = filepaths.map(path => path.replace(this.AudiobookPath, ''))
|
||||||
var fileGroupings = groupFilesIntoAudiobookPaths(relfilepaths, true)
|
var fileGroupings = groupFilesIntoAudiobookPaths(relfilepaths, true)
|
||||||
|
|
||||||
|
|
||||||
Logger.debug(`[Scanner] fileGroupings `, filepaths, fileGroupings)
|
|
||||||
|
|
||||||
|
|
||||||
var results = []
|
var results = []
|
||||||
for (const dir in fileGroupings) {
|
for (const dir in fileGroupings) {
|
||||||
Logger.debug(`[Scanner] Check dir ${dir}`)
|
Logger.debug(`[Scanner] Check dir ${dir}`)
|
||||||
@ -334,19 +353,6 @@ class Scanner {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetadata(id, trackIndex = 0) {
|
|
||||||
var audiobook = this.audiobooks.find(a => a.id === id)
|
|
||||||
if (!audiobook) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var tracks = audiobook.tracks
|
|
||||||
var index = isNaN(trackIndex) ? 0 : Number(trackIndex)
|
|
||||||
var firstTrack = tracks[index]
|
|
||||||
var firstTrackFullPath = firstTrack.fullPath
|
|
||||||
var scanResult = await audioFileScanner.scan(firstTrackFullPath)
|
|
||||||
return scanResult
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanCovers() {
|
async scanCovers() {
|
||||||
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
|
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
|
||||||
var found = 0
|
var found = 0
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
const AudioFileMetadata = require('./AudioFileMetadata')
|
||||||
|
|
||||||
class AudioFile {
|
class AudioFile {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this.index = null
|
this.index = null
|
||||||
@ -21,12 +23,10 @@ class AudioFile {
|
|||||||
this.channels = null
|
this.channels = null
|
||||||
this.channelLayout = null
|
this.channelLayout = null
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
|
this.embeddedCoverArt = null
|
||||||
|
|
||||||
this.tagAlbum = null
|
// Tags scraped from the audio file
|
||||||
this.tagArtist = null
|
this.metadata = null
|
||||||
this.tagGenre = null
|
|
||||||
this.tagTitle = null
|
|
||||||
this.tagTrack = null
|
|
||||||
|
|
||||||
this.manuallyVerified = false
|
this.manuallyVerified = false
|
||||||
this.invalid = false
|
this.invalid = false
|
||||||
@ -62,11 +62,8 @@ class AudioFile {
|
|||||||
channels: this.channels,
|
channels: this.channels,
|
||||||
channelLayout: this.channelLayout,
|
channelLayout: this.channelLayout,
|
||||||
chapters: this.chapters,
|
chapters: this.chapters,
|
||||||
tagAlbum: this.tagAlbum,
|
embeddedCoverArt: this.embeddedCoverArt,
|
||||||
tagArtist: this.tagArtist,
|
metadata: this.metadata ? this.metadata.toJSON() : {}
|
||||||
tagGenre: this.tagGenre,
|
|
||||||
tagTitle: this.tagTitle,
|
|
||||||
tagTrack: this.tagTrack
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,12 +93,20 @@ class AudioFile {
|
|||||||
this.channels = data.channels
|
this.channels = data.channels
|
||||||
this.channelLayout = data.channelLayout
|
this.channelLayout = data.channelLayout
|
||||||
this.chapters = data.chapters
|
this.chapters = data.chapters
|
||||||
|
this.embeddedCoverArt = data.embeddedCoverArt || null
|
||||||
|
|
||||||
this.tagAlbum = data.tagAlbum
|
// Old version of AudioFile used `tagAlbum` etc.
|
||||||
this.tagArtist = data.tagArtist
|
var isOldVersion = Object.keys(data).find(key => key.startsWith('tag'))
|
||||||
this.tagGenre = data.tagGenre
|
if (isOldVersion) {
|
||||||
this.tagTitle = data.tagTitle
|
this.metadata = new AudioFileMetadata(data)
|
||||||
this.tagTrack = data.tagTrack
|
} else {
|
||||||
|
this.metadata = new AudioFileMetadata(data.metadata || {})
|
||||||
|
}
|
||||||
|
// this.tagAlbum = data.tagAlbum
|
||||||
|
// this.tagArtist = data.tagArtist
|
||||||
|
// this.tagGenre = data.tagGenre
|
||||||
|
// this.tagTitle = data.tagTitle
|
||||||
|
// this.tagTrack = data.tagTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
@ -131,12 +136,10 @@ class AudioFile {
|
|||||||
this.channels = data.channels
|
this.channels = data.channels
|
||||||
this.channelLayout = data.channel_layout
|
this.channelLayout = data.channel_layout
|
||||||
this.chapters = data.chapters || []
|
this.chapters = data.chapters || []
|
||||||
|
this.embeddedCoverArt = data.embedded_cover_art || null
|
||||||
|
|
||||||
this.tagAlbum = data.file_tag_album || null
|
this.metadata = new AudioFileMetadata()
|
||||||
this.tagArtist = data.file_tag_artist || null
|
this.metadata.setData(data)
|
||||||
this.tagGenre = data.file_tag_genre || null
|
|
||||||
this.tagTitle = data.file_tag_title || null
|
|
||||||
this.tagTrack = data.file_tag_track || null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
|
69
server/objects/AudioFileMetadata.js
Normal file
69
server/objects/AudioFileMetadata.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
class AudioFileMetadata {
|
||||||
|
constructor(metadata) {
|
||||||
|
this.tagAlbum = null
|
||||||
|
this.tagArtist = null
|
||||||
|
this.tagGenre = null
|
||||||
|
this.tagTitle = null
|
||||||
|
this.tagTrack = null
|
||||||
|
this.tagSubtitle = null
|
||||||
|
this.tagAlbumArtist = null
|
||||||
|
this.tagDate = null
|
||||||
|
this.tagComposer = null
|
||||||
|
this.tagPublisher = null
|
||||||
|
this.tagComment = null
|
||||||
|
this.tagDescription = null
|
||||||
|
this.tagEncoder = null
|
||||||
|
this.tagEncodedBy = null
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
this.construct(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
// Only return the tags that are actually set
|
||||||
|
var json = {}
|
||||||
|
for (const key in this) {
|
||||||
|
if (key.startsWith('tag') && this[key]) {
|
||||||
|
json[key] = this[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
construct(metadata) {
|
||||||
|
this.tagAlbum = metadata.tagAlbum || null
|
||||||
|
this.tagArtist = metadata.tagArtist || null
|
||||||
|
this.tagGenre = metadata.tagGenre || null
|
||||||
|
this.tagTitle = metadata.tagTitle || null
|
||||||
|
this.tagTrack = metadata.tagTrack || null
|
||||||
|
this.tagSubtitle = metadata.tagSubtitle || null
|
||||||
|
this.tagAlbumArtist = metadata.tagAlbumArtist || null
|
||||||
|
this.tagDate = metadata.tagDate || null
|
||||||
|
this.tagComposer = metadata.tagComposer || null
|
||||||
|
this.tagPublisher = metadata.tagPublisher || null
|
||||||
|
this.tagComment = metadata.tagComment || null
|
||||||
|
this.tagDescription = metadata.tagDescription || null
|
||||||
|
this.tagEncoder = metadata.tagEncoder || null
|
||||||
|
this.tagEncodedBy = metadata.tagEncodedBy || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data parsed in prober.js
|
||||||
|
setData(payload) {
|
||||||
|
this.tagAlbum = payload.file_tag_album || null
|
||||||
|
this.tagArtist = payload.file_tag_artist || null
|
||||||
|
this.tagGenre = payload.file_tag_genre || null
|
||||||
|
this.tagTitle = payload.file_tag_title || null
|
||||||
|
this.tagTrack = payload.file_tag_track || null
|
||||||
|
this.tagSubtitle = payload.file_tag_subtitle || null
|
||||||
|
this.tagAlbumArtist = payload.file_tag_albumartist || null
|
||||||
|
this.tagDate = payload.file_tag_date || null
|
||||||
|
this.tagComposer = payload.file_tag_composer || null
|
||||||
|
this.tagPublisher = payload.file_tag_publisher || null
|
||||||
|
this.tagComment = payload.file_tag_comment || null
|
||||||
|
this.tagDescription = payload.file_tag_description || null
|
||||||
|
this.tagEncoder = payload.file_tag_encoder || null
|
||||||
|
this.tagEncodedBy = payload.file_tag_encodedby || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = AudioFileMetadata
|
@ -20,11 +20,12 @@ class AudioTrack {
|
|||||||
this.channels = null
|
this.channels = null
|
||||||
this.channelLayout = null
|
this.channelLayout = null
|
||||||
|
|
||||||
this.tagAlbum = null
|
// Storing tags in audio track is unnecessary, tags are stored on audio file
|
||||||
this.tagArtist = null
|
// this.tagAlbum = null
|
||||||
this.tagGenre = null
|
// this.tagArtist = null
|
||||||
this.tagTitle = null
|
// this.tagGenre = null
|
||||||
this.tagTrack = null
|
// this.tagTitle = null
|
||||||
|
// this.tagTrack = null
|
||||||
|
|
||||||
if (audioTrack) {
|
if (audioTrack) {
|
||||||
this.construct(audioTrack)
|
this.construct(audioTrack)
|
||||||
@ -50,11 +51,11 @@ class AudioTrack {
|
|||||||
this.channels = audioTrack.channels
|
this.channels = audioTrack.channels
|
||||||
this.channelLayout = audioTrack.channelLayout
|
this.channelLayout = audioTrack.channelLayout
|
||||||
|
|
||||||
this.tagAlbum = audioTrack.tagAlbum
|
// this.tagAlbum = audioTrack.tagAlbum
|
||||||
this.tagArtist = audioTrack.tagArtist
|
// this.tagArtist = audioTrack.tagArtist
|
||||||
this.tagGenre = audioTrack.tagGenre
|
// this.tagGenre = audioTrack.tagGenre
|
||||||
this.tagTitle = audioTrack.tagTitle
|
// this.tagTitle = audioTrack.tagTitle
|
||||||
this.tagTrack = audioTrack.tagTrack
|
// this.tagTrack = audioTrack.tagTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -77,11 +78,11 @@ class AudioTrack {
|
|||||||
timeBase: this.timeBase,
|
timeBase: this.timeBase,
|
||||||
channels: this.channels,
|
channels: this.channels,
|
||||||
channelLayout: this.channelLayout,
|
channelLayout: this.channelLayout,
|
||||||
tagAlbum: this.tagAlbum,
|
// tagAlbum: this.tagAlbum,
|
||||||
tagArtist: this.tagArtist,
|
// tagArtist: this.tagArtist,
|
||||||
tagGenre: this.tagGenre,
|
// tagGenre: this.tagGenre,
|
||||||
tagTitle: this.tagTitle,
|
// tagTitle: this.tagTitle,
|
||||||
tagTrack: this.tagTrack
|
// tagTrack: this.tagTrack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,11 +105,11 @@ class AudioTrack {
|
|||||||
this.channels = probeData.channels
|
this.channels = probeData.channels
|
||||||
this.channelLayout = probeData.channelLayout
|
this.channelLayout = probeData.channelLayout
|
||||||
|
|
||||||
this.tagAlbum = probeData.file_tag_album || null
|
// this.tagAlbum = probeData.file_tag_album || null
|
||||||
this.tagArtist = probeData.file_tag_artist || null
|
// this.tagArtist = probeData.file_tag_artist || null
|
||||||
this.tagGenre = probeData.file_tag_genre || null
|
// this.tagGenre = probeData.file_tag_genre || null
|
||||||
this.tagTitle = probeData.file_tag_title || null
|
// this.tagTitle = probeData.file_tag_title || null
|
||||||
this.tagTrack = probeData.file_tag_track || null
|
// this.tagTrack = probeData.file_tag_track || null
|
||||||
}
|
}
|
||||||
|
|
||||||
syncFile(newFile) {
|
syncFile(newFile) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { bytesPretty, elapsedPretty } = require('../utils/fileUtils')
|
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
|
||||||
const { comparePaths, getIno } = require('../utils/index')
|
const { comparePaths, getIno } = require('../utils/index')
|
||||||
|
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||||
const nfoGenerator = require('../utils/nfoGenerator')
|
const nfoGenerator = require('../utils/nfoGenerator')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Book = require('./Book')
|
const Book = require('./Book')
|
||||||
@ -115,6 +116,14 @@ class Audiobook {
|
|||||||
return !this.ino || (this.audioFiles || []).find(abf => !abf.ino) || (this.otherFiles || []).find(f => !f.ino) || (this.tracks || []).find(t => !t.ino)
|
return !this.ino || (this.audioFiles || []).find(abf => !abf.ino) || (this.otherFiles || []).find(f => !f.ino) || (this.tracks || []).find(t => !t.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasEmbeddedCoverArt() {
|
||||||
|
return !!(this.audioFiles || []).find(af => af.embeddedCoverArt)
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasDescriptionTextFile() {
|
||||||
|
return !!(this.otherFiles || []).find(of => of.filename === 'desc.txt')
|
||||||
|
}
|
||||||
|
|
||||||
bookToJSON() {
|
bookToJSON() {
|
||||||
return this.book ? this.book.toJSON() : null
|
return this.book ? this.book.toJSON() : null
|
||||||
}
|
}
|
||||||
@ -192,20 +201,6 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanner had a bug that was saving a file path as the audiobook path.
|
|
||||||
// audiobook path should be a directory.
|
|
||||||
// fixing this before a scan prevents audiobooks being removed and re-added
|
|
||||||
fixRelativePath(abRootPath) {
|
|
||||||
var pathExt = Path.extname(this.path)
|
|
||||||
if (pathExt) {
|
|
||||||
this.path = Path.dirname(this.path)
|
|
||||||
this.fullPath = Path.join(abRootPath, this.path)
|
|
||||||
Logger.warn('Audiobook path has extname', pathExt, 'fixed path:', this.path)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Originally files did not store the inode value
|
// Originally files did not store the inode value
|
||||||
// this function checks all files and sets the inode
|
// this function checks all files and sets the inode
|
||||||
async checkUpdateInos() {
|
async checkUpdateInos() {
|
||||||
@ -414,23 +409,37 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On scan check other files found with other files saved
|
// On scan check other files found with other files saved
|
||||||
syncOtherFiles(newOtherFiles) {
|
async syncOtherFiles(newOtherFiles) {
|
||||||
|
var hasUpdates = false
|
||||||
|
|
||||||
var currOtherFileNum = this.otherFiles.length
|
var currOtherFileNum = this.otherFiles.length
|
||||||
|
|
||||||
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
||||||
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
||||||
|
|
||||||
|
// Some files are not there anymore and filtered out
|
||||||
|
if (currOtherFileNum !== this.otherFiles.length) hasUpdates = true
|
||||||
|
|
||||||
|
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
|
||||||
|
if (descriptionTxt) {
|
||||||
|
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
||||||
|
if (newDescription) {
|
||||||
|
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
|
||||||
|
this.update({ book: { description: newDescription } })
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Should use inode
|
// TODO: Should use inode
|
||||||
newOtherFiles.forEach((file) => {
|
newOtherFiles.forEach((file) => {
|
||||||
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
||||||
if (!existingOtherFile) {
|
if (!existingOtherFile) {
|
||||||
Logger.debug(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
Logger.debug(`[Audiobook] New other file found on sync ${file.filename} | "${this.title}"`)
|
||||||
this.addOtherFile(file)
|
this.addOtherFile(file)
|
||||||
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var hasUpdates = currOtherFileNum !== this.otherFiles.length
|
|
||||||
|
|
||||||
// Check if cover was a local image and that it still exists
|
// Check if cover was a local image and that it still exists
|
||||||
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
|
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
|
||||||
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
|
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
|
||||||
@ -535,5 +544,38 @@ class Audiobook {
|
|||||||
writeNfoFile(nfoFilename = 'metadata.nfo') {
|
writeNfoFile(nfoFilename = 'metadata.nfo') {
|
||||||
return nfoGenerator(this, nfoFilename)
|
return nfoGenerator(this, nfoFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return cover filename
|
||||||
|
async saveEmbeddedCoverArt(coverDirFullPath, coverDirRelPath) {
|
||||||
|
var audioFileWithCover = this.audioFiles.find(af => af.embeddedCoverArt)
|
||||||
|
if (!audioFileWithCover) return false
|
||||||
|
|
||||||
|
var coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
|
||||||
|
var coverFilePath = Path.join(coverDirFullPath, coverFilename)
|
||||||
|
|
||||||
|
var success = await extractCoverArt(audioFileWithCover.fullPath, coverFilePath)
|
||||||
|
if (success) {
|
||||||
|
var coverRelPath = Path.join(coverDirRelPath, coverFilename)
|
||||||
|
this.update({ book: { cover: coverRelPath } })
|
||||||
|
return coverRelPath
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If desc.txt exists then use it as description
|
||||||
|
async saveDescriptionFromTextFile() {
|
||||||
|
var descriptionTextFile = this.otherFiles.find(file => file.filename === 'desc.txt')
|
||||||
|
if (!descriptionTextFile) return false
|
||||||
|
var newDescription = await readTextFile(descriptionTextFile.fullPath)
|
||||||
|
if (!newDescription) return false
|
||||||
|
return this.update({ book: { description: newDescription } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio file metadata tags map to EMPTY book details
|
||||||
|
setDetailsFromFileMetadata() {
|
||||||
|
if (!this.audioFiles.length) return false
|
||||||
|
var audioFile = this.audioFiles[0]
|
||||||
|
return this.book.setDetailsFromFileMetadata(audioFile.metadata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Audiobook
|
module.exports = Audiobook
|
@ -183,5 +183,47 @@ class Book {
|
|||||||
isSearchMatch(search) {
|
isSearchMatch(search) {
|
||||||
return this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
|
return this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDetailsFromFileMetadata(audioFileMetadata) {
|
||||||
|
const MetadataMapArray = [
|
||||||
|
{
|
||||||
|
tag: 'tagComposer',
|
||||||
|
key: 'narrarator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDescription',
|
||||||
|
key: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagPublisher',
|
||||||
|
key: 'publisher'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDate',
|
||||||
|
key: 'publishYear'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagSubtitle',
|
||||||
|
key: 'subtitle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagArtist',
|
||||||
|
key: 'author'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
var updatePayload = {}
|
||||||
|
MetadataMapArray.forEach((mapping) => {
|
||||||
|
if (!this[mapping.key] && audioFileMetadata[mapping.tag]) {
|
||||||
|
updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
|
||||||
|
Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Object.keys(updatePayload).length) {
|
||||||
|
return this.update(updatePayload)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
@ -2,6 +2,8 @@ const Path = require('path')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const prober = require('./prober')
|
const prober = require('./prober')
|
||||||
|
|
||||||
|
const ImageCodecs = ['mjpeg', 'jpeg', 'png']
|
||||||
|
|
||||||
function getDefaultAudioStream(audioStreams) {
|
function getDefaultAudioStream(audioStreams) {
|
||||||
if (audioStreams.length === 1) return audioStreams[0]
|
if (audioStreams.length === 1) return audioStreams[0]
|
||||||
var defaultStream = audioStreams.find(a => a.is_default)
|
var defaultStream = audioStreams.find(a => a.is_default)
|
||||||
@ -37,6 +39,11 @@ async function scan(path) {
|
|||||||
chapters: probeData.chapters || []
|
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) {
|
for (const key in probeData) {
|
||||||
if (probeData[key] && key.startsWith('file_tag')) {
|
if (probeData[key] && key.startsWith('file_tag')) {
|
||||||
finalData[key] = probeData[key]
|
finalData[key] = probeData[key]
|
||||||
@ -129,7 +136,7 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.find(t => t.index === trackNumber)) {
|
if (tracks.find(t => t.index === trackNumber)) {
|
||||||
Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
// Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
||||||
audioFile.invalid = true
|
audioFile.invalid = true
|
||||||
audioFile.error = 'Duplicate track number'
|
audioFile.error = 'Duplicate track number'
|
||||||
numDuplicateTracks++
|
numDuplicateTracks++
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
const Ffmpeg = require('fluent-ffmpeg')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
const Path = require('path')
|
||||||
const package = require('../../package.json')
|
const package = require('../../package.json')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
function escapeSingleQuotes(path) {
|
function escapeSingleQuotes(path) {
|
||||||
// return path.replace(/'/g, '\'\\\'\'')
|
// return path.replace(/'/g, '\'\\\'\'')
|
||||||
@ -65,3 +68,28 @@ async function writeMetadataFile(audiobook, outputPath) {
|
|||||||
return inputstrs
|
return inputstrs
|
||||||
}
|
}
|
||||||
module.exports.writeMetadataFile = writeMetadataFile
|
module.exports.writeMetadataFile = writeMetadataFile
|
||||||
|
|
||||||
|
async function extractCoverArt(filepath, outputpath) {
|
||||||
|
var dirname = Path.dirname(outputpath)
|
||||||
|
await fs.ensureDir(dirname)
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
var ffmpeg = Ffmpeg(filepath)
|
||||||
|
ffmpeg.addOption(['-map 0:v'])
|
||||||
|
ffmpeg.output(outputpath)
|
||||||
|
|
||||||
|
ffmpeg.on('start', (cmd) => {
|
||||||
|
Logger.debug(`[FfmpegHelpers] Extract Cover Cmd: ${cmd}`)
|
||||||
|
})
|
||||||
|
ffmpeg.on('error', (err, stdout, stderr) => {
|
||||||
|
Logger.error(`[FfmpegHelpers] Extract Cover Error ${err}`)
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
ffmpeg.on('end', () => {
|
||||||
|
Logger.debug(`[FfmpegHelpers] Cover Art Extracted Successfully`)
|
||||||
|
resolve(outputpath)
|
||||||
|
})
|
||||||
|
ffmpeg.run()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
module.exports.extractCoverArt = extractCoverArt
|
@ -1,4 +1,5 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
async function getFileStat(path) {
|
async function getFileStat(path) {
|
||||||
try {
|
try {
|
||||||
@ -24,6 +25,17 @@ async function getFileSize(path) {
|
|||||||
}
|
}
|
||||||
module.exports.getFileSize = getFileSize
|
module.exports.getFileSize = getFileSize
|
||||||
|
|
||||||
|
async function readTextFile(path) {
|
||||||
|
try {
|
||||||
|
var data = await fs.readFile(path)
|
||||||
|
return String(data)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[FileUtils] ReadTextFile error ${error}`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports.readTextFile = readTextFile
|
||||||
|
|
||||||
function bytesPretty(bytes, decimals = 0) {
|
function bytesPretty(bytes, decimals = 0) {
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
return '0 Bytes'
|
return '0 Bytes'
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
var Ffmpeg = require('fluent-ffmpeg')
|
var Ffmpeg = require('fluent-ffmpeg')
|
||||||
|
const Path = require('path')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||||
if (!isNaN(stream.bit_rate) && stream.bit_rate) {
|
if (!isNaN(stream.bit_rate) && stream.bit_rate) {
|
||||||
@ -72,6 +74,15 @@ function tryGrabTag(stream, tag) {
|
|||||||
return stream.tags[tag] || stream.tags[tag.toUpperCase()] || null
|
return stream.tags[tag] || stream.tags[tag.toUpperCase()] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
||||||
var info = {
|
var info = {
|
||||||
index: stream.index,
|
index: stream.index,
|
||||||
@ -124,6 +135,54 @@ function parseChapters(chapters) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTags(format) {
|
||||||
|
if (!format.tags) {
|
||||||
|
Logger.debug('No Tags')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
// Logger.debug('Tags', format.tags)
|
||||||
|
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'),
|
||||||
|
file_tag_album: tryGrabTags(format, 'album', 'talb', 'tal'),
|
||||||
|
file_tag_artist: tryGrabTags(format, 'artist', 'tpe1', 'tp1'),
|
||||||
|
file_tag_albumartist: tryGrabTags(format, 'albumartist', 'tpe2'),
|
||||||
|
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'),
|
||||||
|
|
||||||
|
// 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_series: tryGrabTag(format, 'series'),
|
||||||
|
file_tag_seriespart: tryGrabTag(format, 'series-part'),
|
||||||
|
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
||||||
|
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2')
|
||||||
|
}
|
||||||
|
for (const key in tags) {
|
||||||
|
if (!tags[key]) {
|
||||||
|
delete tags[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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']
|
||||||
|
var success = keysToLookOutFor.find(key => !!tags[key])
|
||||||
|
if (success) {
|
||||||
|
Logger.debug('Notable!', success)
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
function parseProbeData(data) {
|
function parseProbeData(data) {
|
||||||
try {
|
try {
|
||||||
var { format, streams, chapters } = data
|
var { format, streams, chapters } = data
|
||||||
@ -131,20 +190,16 @@ function parseProbeData(data) {
|
|||||||
|
|
||||||
var sizeBytes = !isNaN(size) ? Number(size) : null
|
var sizeBytes = !isNaN(size) ? Number(size) : null
|
||||||
var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
|
var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
|
||||||
|
|
||||||
|
// Logger.debug('Parsing Data for', Path.basename(format.filename))
|
||||||
|
var tags = parseTags(format)
|
||||||
var cleanedData = {
|
var cleanedData = {
|
||||||
format: format_long_name,
|
format: format_long_name,
|
||||||
duration: !isNaN(duration) ? Number(duration) : null,
|
duration: !isNaN(duration) ? Number(duration) : null,
|
||||||
size: sizeBytes,
|
size: sizeBytes,
|
||||||
sizeMb,
|
sizeMb,
|
||||||
bit_rate: !isNaN(bit_rate) ? Number(bit_rate) : null,
|
bit_rate: !isNaN(bit_rate) ? Number(bit_rate) : null,
|
||||||
file_tag_encoder: tryGrabTag(format, 'encoder') || tryGrabTag(format, 'encoded_by'),
|
...tags
|
||||||
file_tag_title: tryGrabTag(format, 'title'),
|
|
||||||
file_tag_track: tryGrabTag(format, 'track') || tryGrabTag(format, 'trk'),
|
|
||||||
file_tag_album: tryGrabTag(format, 'album') || tryGrabTag(format, 'tal'),
|
|
||||||
file_tag_artist: tryGrabTag(format, 'artist') || tryGrabTag(format, 'tp1'),
|
|
||||||
file_tag_date: tryGrabTag(format, 'date') || tryGrabTag(format, 'tye'),
|
|
||||||
file_tag_genre: tryGrabTag(format, 'genre'),
|
|
||||||
file_tag_creation_time: tryGrabTag(format, 'creation_time')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
||||||
|
Loading…
Reference in New Issue
Block a user