Scanner v4, audio file metadata used in setting book details, embedded cover art extracted and used

This commit is contained in:
advplyr 2021-09-29 20:43:36 -05:00
parent b74b12301c
commit b26c1ba886
15 changed files with 371 additions and 108 deletions

View file

@ -1,3 +1,5 @@
const AudioFileMetadata = require('./AudioFileMetadata')
class AudioFile {
constructor(data) {
this.index = null
@ -21,12 +23,10 @@ class AudioFile {
this.channels = null
this.channelLayout = null
this.chapters = []
this.embeddedCoverArt = null
this.tagAlbum = null
this.tagArtist = null
this.tagGenre = null
this.tagTitle = null
this.tagTrack = null
// Tags scraped from the audio file
this.metadata = null
this.manuallyVerified = false
this.invalid = false
@ -62,11 +62,8 @@ class AudioFile {
channels: this.channels,
channelLayout: this.channelLayout,
chapters: this.chapters,
tagAlbum: this.tagAlbum,
tagArtist: this.tagArtist,
tagGenre: this.tagGenre,
tagTitle: this.tagTitle,
tagTrack: this.tagTrack
embeddedCoverArt: this.embeddedCoverArt,
metadata: this.metadata ? this.metadata.toJSON() : {}
}
}
@ -96,12 +93,20 @@ class AudioFile {
this.channels = data.channels
this.channelLayout = data.channelLayout
this.chapters = data.chapters
this.embeddedCoverArt = data.embeddedCoverArt || null
this.tagAlbum = data.tagAlbum
this.tagArtist = data.tagArtist
this.tagGenre = data.tagGenre
this.tagTitle = data.tagTitle
this.tagTrack = data.tagTrack
// Old version of AudioFile used `tagAlbum` etc.
var isOldVersion = Object.keys(data).find(key => key.startsWith('tag'))
if (isOldVersion) {
this.metadata = new AudioFileMetadata(data)
} 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) {
@ -131,12 +136,10 @@ class AudioFile {
this.channels = data.channels
this.channelLayout = data.channel_layout
this.chapters = data.chapters || []
this.embeddedCoverArt = data.embedded_cover_art || null
this.tagAlbum = data.file_tag_album || null
this.tagArtist = data.file_tag_artist || null
this.tagGenre = data.file_tag_genre || null
this.tagTitle = data.file_tag_title || null
this.tagTrack = data.file_tag_track || null
this.metadata = new AudioFileMetadata()
this.metadata.setData(data)
}
clone() {

View 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

View file

@ -20,11 +20,12 @@ class AudioTrack {
this.channels = null
this.channelLayout = null
this.tagAlbum = null
this.tagArtist = null
this.tagGenre = null
this.tagTitle = null
this.tagTrack = null
// Storing tags in audio track is unnecessary, tags are stored on audio file
// this.tagAlbum = null
// this.tagArtist = null
// this.tagGenre = null
// this.tagTitle = null
// this.tagTrack = null
if (audioTrack) {
this.construct(audioTrack)
@ -50,11 +51,11 @@ class AudioTrack {
this.channels = audioTrack.channels
this.channelLayout = audioTrack.channelLayout
this.tagAlbum = audioTrack.tagAlbum
this.tagArtist = audioTrack.tagArtist
this.tagGenre = audioTrack.tagGenre
this.tagTitle = audioTrack.tagTitle
this.tagTrack = audioTrack.tagTrack
// this.tagAlbum = audioTrack.tagAlbum
// this.tagArtist = audioTrack.tagArtist
// this.tagGenre = audioTrack.tagGenre
// this.tagTitle = audioTrack.tagTitle
// this.tagTrack = audioTrack.tagTrack
}
get name() {
@ -77,11 +78,11 @@ class AudioTrack {
timeBase: this.timeBase,
channels: this.channels,
channelLayout: this.channelLayout,
tagAlbum: this.tagAlbum,
tagArtist: this.tagArtist,
tagGenre: this.tagGenre,
tagTitle: this.tagTitle,
tagTrack: this.tagTrack
// tagAlbum: this.tagAlbum,
// tagArtist: this.tagArtist,
// tagGenre: this.tagGenre,
// tagTitle: this.tagTitle,
// tagTrack: this.tagTrack
}
}
@ -104,11 +105,11 @@ class AudioTrack {
this.channels = probeData.channels
this.channelLayout = probeData.channelLayout
this.tagAlbum = probeData.file_tag_album || null
this.tagArtist = probeData.file_tag_artist || null
this.tagGenre = probeData.file_tag_genre || null
this.tagTitle = probeData.file_tag_title || null
this.tagTrack = probeData.file_tag_track || null
// this.tagAlbum = probeData.file_tag_album || null
// this.tagArtist = probeData.file_tag_artist || null
// this.tagGenre = probeData.file_tag_genre || null
// this.tagTitle = probeData.file_tag_title || null
// this.tagTrack = probeData.file_tag_track || null
}
syncFile(newFile) {

View file

@ -1,6 +1,7 @@
const Path = require('path')
const { bytesPretty, elapsedPretty } = require('../utils/fileUtils')
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
const { comparePaths, getIno } = require('../utils/index')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
const Logger = require('../Logger')
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)
}
get hasEmbeddedCoverArt() {
return !!(this.audioFiles || []).find(af => af.embeddedCoverArt)
}
get hasDescriptionTextFile() {
return !!(this.otherFiles || []).find(of => of.filename === 'desc.txt')
}
bookToJSON() {
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
// this function checks all files and sets the inode
async checkUpdateInos() {
@ -414,23 +409,37 @@ class Audiobook {
}
// On scan check other files found with other files saved
syncOtherFiles(newOtherFiles) {
async syncOtherFiles(newOtherFiles) {
var hasUpdates = false
var currOtherFileNum = this.otherFiles.length
var newOtherFilePaths = newOtherFiles.map(f => 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
newOtherFiles.forEach((file) => {
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
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)
hasUpdates = true
}
})
var hasUpdates = currOtherFileNum !== this.otherFiles.length
// Check if cover was a local image and that it still exists
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
@ -535,5 +544,38 @@ class Audiobook {
writeNfoFile(nfoFilename = 'metadata.nfo') {
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

View file

@ -183,5 +183,47 @@ class Book {
isSearchMatch(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