New data model change of Book media type to include array of Audiobook and Ebook objects

This commit is contained in:
advplyr 2022-03-16 19:15:25 -05:00
parent 0af6ad63c1
commit 4fe60465e5
13 changed files with 677 additions and 334 deletions

View file

@ -1,8 +1,8 @@
const { version } = require('../../package.json')
const Logger = require('../Logger')
const LibraryFile = require('./files/LibraryFile')
const Book = require('./entities/Book')
const Podcast = require('./entities/Podcast')
const Book = require('./mediaTypes/Book')
const Podcast = require('./mediaTypes/Podcast')
const { areEquivalent, copyValue, getId } = require('../utils/index')
class LibraryItem {
@ -143,8 +143,8 @@ class LibraryItem {
get hasAudioFiles() {
return this.libraryFiles.some(lf => lf.fileType === 'audio')
}
get hasMediaFiles() {
return this.media.hasMediaFiles
get hasMediaEntities() {
return this.media.hasMediaEntities
}
// Data comes from scandir library item data
@ -357,7 +357,7 @@ class LibraryItem {
}
// Check if invalid
this.isInvalid = !this.media.hasMediaFiles
this.isInvalid = !this.media.hasMediaEntities
// If cover path is in item folder, make sure libraryFile exists for it
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
@ -432,5 +432,9 @@ class LibraryItem {
query = query.toLowerCase()
return this.media.searchQuery(query)
}
getDirectPlayTracklist(options) {
return this.media.getDirectPlayTracklist(options)
}
}
module.exports = LibraryItem

View file

@ -0,0 +1,218 @@
const Path = require('path')
const AudioFile = require('../files/AudioFile')
const { areEquivalent, copyValue } = require('../../utils/index')
class Audiobook {
constructor(audiobook) {
this.id = null
this.index = null
this.name = null
this.audioFiles = []
this.chapters = []
this.missingParts = []
this.addedAt = null
this.updatedAt = null
if (audiobook) {
this.construct(audiobook)
}
}
construct(audiobook) {
this.id = audiobook.id
this.index = audiobook.index
this.name = audiobook.name || null
this.audioFiles = audiobook.audioFiles.map(f => new AudioFile(f))
this.chapters = audiobook.chapters.map(c => ({ ...c }))
this.missingParts = audiobook.missingParts ? [...book.missingParts] : []
this.addedAt = audiobook.addedAt
this.updatedAt = audiobook.updatedAt
}
toJSON() {
return {
id: this.id,
index: this.index,
name: this.name,
audioFiles: this.audioFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c })),
missingParts: [...this.missingParts],
addedAt: this.addedAt,
updatedAt: this.updatedAt
}
}
toJSONMinified() {
return {
id: this.id,
index: this.index,
name: this.name,
numTracks: this.tracks.length,
numAudioFiles: this.audioFiles.length,
numChapters: this.chapters.length,
numMissingParts: this.missingParts.length,
duration: this.duration,
size: this.size,
addedAt: this.addedAt,
updatedAt: this.updatedAt
}
}
toJSONExpanded() {
return {
id: this.id,
index: this.index,
name: this.name,
audioFiles: this.audioFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c })),
duration: this.duration,
size: this.size,
tracks: this.tracks.map(t => t.toJSON()),
missingParts: [...this.missingParts],
addedAt: this.addedAt,
updatedAt: this.updatedAt
}
}
get tracks() {
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
}
get duration() {
var total = 0
this.tracks.forEach((track) => total += track.duration)
return total
}
get size() {
var total = 0
this.audioFiles.forEach((af) => total += af.metadata.size)
return total
}
get hasEmbeddedCoverArt() {
return this.audioFiles.some(af => af.embeddedCoverArt)
}
update(payload) {
var json = this.toJSON()
var hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
Logger.debug('[Audiobook] Key updated', key, this[key])
hasUpdates = true
}
}
}
return hasUpdates
}
updateAudioTracks(orderedFileData) {
var index = 1
this.audioFiles = orderedFileData.map((fileData) => {
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
audioFile.manuallyVerified = true
audioFile.invalid = false
audioFile.error = null
if (fileData.exclude !== undefined) {
audioFile.exclude = !!fileData.exclude
}
if (audioFile.exclude) {
audioFile.index = -1
} else {
audioFile.index = index++
}
return audioFile
})
this.rebuildTracks()
}
rebuildTracks() {
this.audioFiles.sort((a, b) => a.index - b.index)
this.missingParts = []
this.setChapters()
this.checkUpdateMissingTracks()
}
checkUpdateMissingTracks() {
var currMissingParts = (this.missingParts || []).join(',') || ''
var current_index = 1
var missingParts = []
for (let i = 0; i < this.tracks.length; i++) {
var _track = this.tracks[i]
if (_track.index > current_index) {
var num_parts_missing = _track.index - current_index
for (let x = 0; x < num_parts_missing && x < 9999; x++) {
missingParts.push(current_index + x)
}
}
current_index = _track.index + 1
}
this.missingParts = missingParts
var newMissingParts = (this.missingParts || []).join(',') || ''
var wasUpdated = newMissingParts !== currMissingParts
if (wasUpdated && this.missingParts.length) {
Logger.info(`[Audiobook] "${this.name}" has ${missingParts.length} missing parts`)
}
return wasUpdated
}
setChapters() {
// If 1 audio file without chapters, then no chapters will be set
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
if (includedAudioFiles.length === 1) {
// 1 audio file with chapters
if (includedAudioFiles[0].chapters) {
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
}
} else {
this.chapters = []
var currChapterId = 0
var currStartTime = 0
includedAudioFiles.forEach((file) => {
// If audio file has chapters use chapters
if (file.chapters && file.chapters.length) {
file.chapters.forEach((chapter) => {
var chapterDuration = chapter.end - chapter.start
if (chapterDuration > 0) {
var title = `Chapter ${currChapterId}`
if (chapter.title) {
title += ` (${chapter.title})`
}
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: currStartTime + chapterDuration,
title
})
currStartTime += chapterDuration
}
})
} else if (file.duration) {
// Otherwise just use track has chapter
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: currStartTime + file.duration,
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
})
currStartTime += file.duration
}
})
}
}
findFileWithInode(inode) {
return this.audioFiles.find(af => af.ino === inode)
}
removeFileWithInode(inode) {
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
}
}
module.exports = Audiobook

View file

@ -0,0 +1,70 @@
const EBookFile = require('../files/EBookFile')
const { areEquivalent, copyValue } = require('../../utils/index')
class EBook {
constructor(ebook) {
this.id = null
this.index = null
this.name = null
this.ebookFile = null
this.addedAt = null
this.updatedAt = null
if (ebook) {
this.construct(ebook)
}
}
construct(ebook) {
this.id = ebook.id
this.index = ebook.index
this.name = ebook.name
this.ebookFile = new EBookFile(ebook.ebookFile)
this.addedAt = ebook.addedAt
this.updatedAt = ebook.updatedAt
}
toJSON() {
return {
id: this.id,
index: this.index,
name: this.name,
ebookFile: this.ebookFile.toJSON(),
addedAt: this.addedAt,
updatedAt: this.updatedAt
}
}
toJSONMinified() {
return {
id: this.id,
index: this.index,
name: this.name,
ebookFormat: this.ebookFile.ebookFormat,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
size: this.size
}
}
toJSONMinified() {
return {
id: this.id,
index: this.index,
name: this.name,
ebookFile: this.ebookFile.toJSON(),
addedAt: this.addedAt,
updatedAt: this.updatedAt,
size: this.size
}
}
get size() {
return this.ebookFile.metadata.size
}
findFileWithInode(inode) {
return this.ebookFile.ino === inode
}
}
module.exports = EBook

View file

@ -3,6 +3,7 @@ const AudioFile = require('../files/AudioFile')
class PodcastEpisode {
constructor(episode) {
this.id = null
this.index = null
this.podcastId = null
this.episodeNumber = null
@ -17,6 +18,7 @@ class PodcastEpisode {
construct(episode) {
this.id = episode.id
this.index = episode.index
this.podcastId = episode.podcastId
this.episodeNumber = episode.episodeNumber
this.audioFile = new AudioFile(episode.audioFile)
@ -27,6 +29,7 @@ class PodcastEpisode {
toJSON() {
return {
id: this.id,
index: this.index,
podcastId: this.podcastId,
episodeNumber: this.episodeNumber,
audioFile: this.audioFile.toJSON(),

View file

@ -1,23 +1,23 @@
const Path = require('path')
const Logger = require('../../Logger')
const BookMetadata = require('../metadata/BookMetadata')
const AudioFile = require('../files/AudioFile')
const EBookFile = require('../files/EBookFile')
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
const { areEquivalent, copyValue } = require('../../utils/index')
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
const { readTextFile } = require('../../utils/fileUtils')
const Audiobook = require('../entities/Audiobook')
const EBook = require('../entities/EBook')
class Book {
constructor(book) {
this.metadata = null
this.coverPath = null
this.tags = []
this.audioFiles = []
this.ebookFiles = []
this.chapters = []
this.missingParts = []
this.audiobooks = []
this.ebooks = []
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
@ -31,10 +31,8 @@ class Book {
this.metadata = new BookMetadata(book.metadata)
this.coverPath = book.coverPath
this.tags = [...book.tags]
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
this.chapters = book.chapters.map(c => ({ ...c }))
this.missingParts = book.missingParts ? [...book.missingParts] : []
this.audiobooks = book.audiobooks.map(ab => new Audiobook(ab))
this.ebooks = book.ebooks.map(eb => new EBook(eb))
this.lastCoverSearch = book.lastCoverSearch || null
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
}
@ -44,10 +42,8 @@ class Book {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFiles: this.audioFiles.map(f => f.toJSON()),
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c })),
missingParts: [...this.missingParts]
audiobooks: this.audiobooks.map(ab => ab.toJSON()),
ebooks: this.ebooks.map(eb => eb.toJSON())
}
}
@ -56,11 +52,8 @@ class Book {
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
numTracks: this.tracks.length,
numAudioFiles: this.audioFiles.length,
numEbooks: this.ebookFiles.length,
numChapters: this.chapters.length,
numMissingParts: this.missingParts.length,
audiobooks: this.audiobooks.map(ab => ab.toJSONMinified()),
ebooks: this.ebooks.map(eb => eb.toJSONMinified()),
duration: this.duration,
size: this.size
}
@ -71,13 +64,10 @@ class Book {
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFiles: this.audioFiles.map(f => f.toJSON()),
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
chapters: this.chapters.map(c => ({ ...c })),
audiobooks: this.audiobooks.map(ab => ab.toJSONExpanded()),
ebooks: this.ebooks.map(eb => eb.toJSONExpanded()),
duration: this.duration,
size: this.size,
tracks: this.tracks.map(t => t.toJSON()),
missingParts: [...this.missingParts]
}
}
@ -94,8 +84,8 @@ class Book {
this.audioFiles.forEach((af) => total += af.metadata.size)
return total
}
get hasMediaFiles() {
return !!(this.tracks.length + this.ebookFiles.length)
get hasMediaEntities() {
return !!(this.audiobooks.length + this.ebooks.length)
}
get shouldSearchForCover() {
if (this.coverPath) return false
@ -103,11 +93,14 @@ class Book {
return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day
}
get hasEmbeddedCoverArt() {
return this.audioFiles.some(af => af.embeddedCoverArt)
return this.audiobooks.some(ab => ab.hasEmbeddedCoverArt)
}
update(payload) {
var json = this.toJSON()
delete json.audiobooks // do not update media entities here
delete json.ebooks
var hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
@ -125,27 +118,6 @@ class Book {
return hasUpdates
}
updateAudioTracks(orderedFileData) {
var index = 1
this.audioFiles = orderedFileData.map((fileData) => {
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
audioFile.manuallyVerified = true
audioFile.invalid = false
audioFile.error = null
if (fileData.exclude !== undefined) {
audioFile.exclude = !!fileData.exclude
}
if (audioFile.exclude) {
audioFile.index = -1
} else {
audioFile.index = index++
}
return audioFile
})
this.rebuildTracks()
}
updateCover(coverPath) {
coverPath = coverPath.replace(/\\/g, '/')
if (this.coverPath === coverPath) return false
@ -153,50 +125,27 @@ class Book {
return true
}
checkUpdateMissingTracks() {
var currMissingParts = (this.missingParts || []).join(',') || ''
var current_index = 1
var missingParts = []
for (let i = 0; i < this.tracks.length; i++) {
var _track = this.tracks[i]
if (_track.index > current_index) {
var num_parts_missing = _track.index - current_index
for (let x = 0; x < num_parts_missing && x < 9999; x++) {
missingParts.push(current_index + x)
}
}
current_index = _track.index + 1
}
this.missingParts = missingParts
var newMissingParts = (this.missingParts || []).join(',') || ''
var wasUpdated = newMissingParts !== currMissingParts
if (wasUpdated && this.missingParts.length) {
Logger.info(`[Book] "${this.metadata.title}" has ${missingParts.length} missing parts`)
}
return wasUpdated
}
removeFileWithInode(inode) {
if (this.audioFiles.some(af => af.ino === inode)) {
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
if (audiobookWithIno) {
audiobookWithIno.removeFileWithInode(inode)
if (!audiobookWithIno.audioFiles.length) { // All audio files removed = remove audiobook
this.audiobooks = this.audiobooks.filter(ab => ab.id !== audiobookWithIno.id)
}
return true
}
if (this.ebookFiles.some(ef => ef.ino === inode)) {
this.ebookFiles = this.ebookFiles.filter(ef => ef.ino !== inode)
var ebookWithIno = this.ebooks.find(eb => eb.findFileWithInode(inode))
if (ebookWithIno) {
this.ebooks = this.ebooks.filter(eb => eb.id !== ebookWithIno.id) // Remove ebook
return true
}
return false
}
findFileWithInode(inode) {
var audioFile = this.audioFiles.find(af => af.ino == inode)
var audioFile = this.audiobooks.find(ab => ab.findFileWithInode(inode))
if (audioFile) return audioFile
var ebookFile = this.ebookFiles.find(ef => ef.inode == inode)
var ebookFile = this.ebooks.find(eb => eb.findFileWithInode(inode))
if (ebookFile) return ebookFile
return null
}
@ -208,64 +157,13 @@ class Book {
// Audio file metadata tags map to book details (will not overwrite)
setMetadataFromAudioFile(overrideExistingDetails = false) {
if (!this.audioFiles.length) return false
var audioFile = this.audioFiles[0]
if (!this.audiobooks.length) return false
var audiobook = this.audiobooks[0]
var audioFile = audiobook.audioFiles[0]
if (!audioFile.metaTags) return false
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
}
rebuildTracks() {
this.audioFiles.sort((a, b) => a.index - b.index)
this.missingParts = []
this.setChapters()
this.checkUpdateMissingTracks()
}
setChapters() {
// If 1 audio file without chapters, then no chapters will be set
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
if (includedAudioFiles.length === 1) {
// 1 audio file with chapters
if (includedAudioFiles[0].chapters) {
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
}
} else {
this.chapters = []
var currChapterId = 0
var currStartTime = 0
includedAudioFiles.forEach((file) => {
// If audio file has chapters use chapters
if (file.chapters && file.chapters.length) {
file.chapters.forEach((chapter) => {
var chapterDuration = chapter.end - chapter.start
if (chapterDuration > 0) {
var title = `Chapter ${currChapterId}`
if (chapter.title) {
title += ` (${chapter.title})`
}
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: currStartTime + chapterDuration,
title
})
currStartTime += chapterDuration
}
})
} else if (file.duration) {
// Otherwise just use track has chapter
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: currStartTime + file.duration,
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
})
currStartTime += file.duration
}
})
}
}
setData(scanMediaMetadata) {
this.metadata = new BookMetadata()
this.metadata.setData(scanMediaMetadata)
@ -365,9 +263,13 @@ class Book {
}
addEbookFile(libraryFile) {
var newEbook = new EBookFile()
newEbook.setData(libraryFile)
this.ebookFiles.push(newEbook)
// var newEbook = new EBookFile()
// newEbook.setData(libraryFile)
// this.ebookFiles.push(newEbook)
}
getDirectPlayTracklist(options) {
}
}
module.exports = Book

View file

@ -1,4 +1,4 @@
const PodcastEpisode = require('./PodcastEpisode')
const PodcastEpisode = require('../entities/PodcastEpisode')
const PodcastMetadata = require('../metadata/PodcastMetadata')
const { areEquivalent, copyValue } = require('../../utils/index')
@ -68,7 +68,7 @@ class Podcast {
get size() {
return 0
}
get hasMediaFiles() {
get hasMediaEntities() {
return !!this.episodes.length
}
get shouldSearchForCover() {
@ -80,6 +80,7 @@ class Podcast {
update(payload) {
var json = this.toJSON()
delete json.episodes // do not update media entities here
var hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
@ -129,5 +130,9 @@ class Podcast {
var payload = this.metadata.searchQuery(query)
return payload || {}
}
getDirectPlayTracklist(options) {
}
}
module.exports = Podcast

View file

@ -49,6 +49,9 @@ class FileMetadata {
if (!this.ext) return ''
return this.ext.slice(1)
}
get filenameNoExt() {
return this.filename.replace(this.ext, '')
}
update(payload) {
var hasUpdates = false