2021-08-24 14:15:56 +02:00
|
|
|
const Path = require('path')
|
2021-08-18 00:01:11 +02:00
|
|
|
const { bytesPretty, elapsedPretty } = require('./utils/fileUtils')
|
2021-08-26 00:36:54 +02:00
|
|
|
const { comparePaths, getIno } = require('./utils/index')
|
2021-08-24 14:15:56 +02:00
|
|
|
const Logger = require('./Logger')
|
2021-08-18 00:01:11 +02:00
|
|
|
const Book = require('./Book')
|
|
|
|
const AudioTrack = require('./AudioTrack')
|
2021-08-26 00:36:54 +02:00
|
|
|
const AudioFile = require('./AudioFile')
|
|
|
|
const AudiobookFile = require('./AudiobookFile')
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
class Audiobook {
|
|
|
|
constructor(audiobook = null) {
|
|
|
|
this.id = null
|
2021-08-26 00:36:54 +02:00
|
|
|
this.ino = null // Inode
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.path = null
|
|
|
|
this.fullPath = null
|
2021-08-26 00:36:54 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.addedAt = null
|
2021-08-24 14:15:56 +02:00
|
|
|
this.lastUpdate = null
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
this.tracks = []
|
|
|
|
this.missingParts = []
|
|
|
|
this.invalidParts = []
|
|
|
|
|
|
|
|
this.audioFiles = []
|
|
|
|
this.otherFiles = []
|
|
|
|
|
|
|
|
this.tags = []
|
|
|
|
this.book = null
|
|
|
|
|
|
|
|
if (audiobook) {
|
|
|
|
this.construct(audiobook)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
construct(audiobook) {
|
|
|
|
this.id = audiobook.id
|
2021-08-26 00:36:54 +02:00
|
|
|
this.ino = audiobook.ino || null
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.path = audiobook.path
|
|
|
|
this.fullPath = audiobook.fullPath
|
|
|
|
this.addedAt = audiobook.addedAt
|
2021-08-24 14:15:56 +02:00
|
|
|
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
this.tracks = audiobook.tracks.map(track => new AudioTrack(track))
|
2021-08-18 00:01:11 +02:00
|
|
|
this.missingParts = audiobook.missingParts
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
this.audioFiles = audiobook.audioFiles.map(file => new AudioFile(file))
|
|
|
|
this.otherFiles = audiobook.otherFiles.map(file => new AudiobookFile(file))
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
this.tags = audiobook.tags
|
|
|
|
if (audiobook.book) {
|
|
|
|
this.book = new Book(audiobook.book)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get title() {
|
|
|
|
return this.book ? this.book.title : 'No Title'
|
|
|
|
}
|
|
|
|
|
|
|
|
get cover() {
|
|
|
|
return this.book ? this.book.cover : ''
|
|
|
|
}
|
|
|
|
|
|
|
|
get author() {
|
|
|
|
return this.book ? this.book.author : 'Unknown'
|
|
|
|
}
|
|
|
|
|
2021-08-25 03:24:40 +02:00
|
|
|
get authorLF() {
|
|
|
|
return this.book ? this.book.authorLF : null
|
|
|
|
}
|
|
|
|
|
2021-08-20 00:29:36 +02:00
|
|
|
get genres() {
|
|
|
|
return this.book ? this.book.genres || [] : []
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
get totalDuration() {
|
|
|
|
var total = 0
|
|
|
|
this.tracks.forEach((track) => total += track.duration)
|
|
|
|
return total
|
|
|
|
}
|
|
|
|
|
|
|
|
get totalSize() {
|
|
|
|
var total = 0
|
|
|
|
this.tracks.forEach((track) => total += track.size)
|
|
|
|
return total
|
|
|
|
}
|
|
|
|
|
|
|
|
get sizePretty() {
|
|
|
|
return bytesPretty(this.totalSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
get durationPretty() {
|
|
|
|
return elapsedPretty(this.totalDuration)
|
|
|
|
}
|
|
|
|
|
|
|
|
bookToJSON() {
|
|
|
|
return this.book ? this.book.toJSON() : null
|
|
|
|
}
|
|
|
|
|
|
|
|
tracksToJSON() {
|
|
|
|
if (!this.tracks || !this.tracks.length) return []
|
|
|
|
return this.tracks.map(t => t.toJSON())
|
|
|
|
}
|
|
|
|
|
|
|
|
toJSON() {
|
|
|
|
return {
|
|
|
|
id: this.id,
|
2021-08-26 00:36:54 +02:00
|
|
|
ino: this.ino,
|
2021-08-18 00:01:11 +02:00
|
|
|
title: this.title,
|
|
|
|
author: this.author,
|
|
|
|
cover: this.cover,
|
|
|
|
path: this.path,
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
addedAt: this.addedAt,
|
2021-08-24 14:15:56 +02:00
|
|
|
lastUpdate: this.lastUpdate,
|
2021-08-18 00:01:11 +02:00
|
|
|
missingParts: this.missingParts,
|
|
|
|
invalidParts: this.invalidParts,
|
|
|
|
tags: this.tags,
|
|
|
|
book: this.bookToJSON(),
|
|
|
|
tracks: this.tracksToJSON(),
|
2021-08-26 00:36:54 +02:00
|
|
|
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
|
|
|
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON())
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toJSONMinified() {
|
|
|
|
return {
|
|
|
|
id: this.id,
|
2021-08-26 00:36:54 +02:00
|
|
|
ino: this.ino,
|
2021-08-18 00:01:11 +02:00
|
|
|
book: this.bookToJSON(),
|
|
|
|
tags: this.tags,
|
|
|
|
path: this.path,
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
addedAt: this.addedAt,
|
2021-08-24 14:15:56 +02:00
|
|
|
lastUpdate: this.lastUpdate,
|
2021-08-18 00:01:11 +02:00
|
|
|
duration: this.totalDuration,
|
|
|
|
size: this.totalSize,
|
|
|
|
hasBookMatch: !!this.book,
|
|
|
|
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
|
|
|
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
|
|
|
numTracks: this.tracks.length
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toJSONExpanded() {
|
|
|
|
return {
|
|
|
|
id: this.id,
|
|
|
|
path: this.path,
|
|
|
|
fullPath: this.fullPath,
|
|
|
|
addedAt: this.addedAt,
|
2021-08-24 14:15:56 +02:00
|
|
|
lastUpdate: this.lastUpdate,
|
2021-08-18 00:01:11 +02:00
|
|
|
duration: this.totalDuration,
|
|
|
|
durationPretty: this.durationPretty,
|
|
|
|
size: this.totalSize,
|
|
|
|
sizePretty: this.sizePretty,
|
|
|
|
missingParts: this.missingParts,
|
|
|
|
invalidParts: this.invalidParts,
|
2021-08-26 00:36:54 +02:00
|
|
|
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
|
|
|
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
|
2021-08-18 00:01:11 +02:00
|
|
|
tags: this.tags,
|
|
|
|
book: this.bookToJSON(),
|
|
|
|
tracks: this.tracksToJSON()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-24 14:50:36 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
// Update was made to add ino values, ensure they are set
|
|
|
|
async checkUpdateInos() {
|
|
|
|
var hasUpdates = false
|
|
|
|
if (!this.ino) {
|
|
|
|
this.ino = await getIno(this.fullPath)
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
for (let i = 0; i < this.audioFiles.length; i++) {
|
|
|
|
var af = this.audioFiles[i]
|
|
|
|
if (!af.ino || af.ino === this.ino) {
|
|
|
|
af.ino = await getIno(af.fullPath)
|
|
|
|
if (!af.ino) {
|
|
|
|
Logger.error('[Audiobook] checkUpdateInos: Failed to set ino for audio file', af.fullPath)
|
|
|
|
} else {
|
|
|
|
var track = this.tracks.find(t => comparePaths(t.path, af.path))
|
|
|
|
if (track) {
|
|
|
|
track.ino = af.ino
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hasUpdates
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
setData(data) {
|
|
|
|
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
|
2021-08-26 00:36:54 +02:00
|
|
|
this.ino = data.ino || null
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.path = data.path
|
|
|
|
this.fullPath = data.fullPath
|
|
|
|
this.addedAt = Date.now()
|
2021-08-24 14:15:56 +02:00
|
|
|
this.lastUpdate = this.addedAt
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
if (data.otherFiles) {
|
|
|
|
data.otherFiles.forEach((file) => {
|
|
|
|
this.addOtherFile(file)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.setBook(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
setBook(data) {
|
|
|
|
this.book = new Book()
|
|
|
|
this.book.setData(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
addTrack(trackData) {
|
|
|
|
var track = new AudioTrack()
|
|
|
|
track.setData(trackData)
|
|
|
|
this.tracks.push(track)
|
|
|
|
return track
|
|
|
|
}
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
addAudioFile(audioFileData) {
|
|
|
|
var audioFile = new AudioFile()
|
|
|
|
audioFile.setData(audioFileData)
|
|
|
|
this.audioFiles.push(audioFile)
|
|
|
|
return audioFile
|
|
|
|
}
|
|
|
|
|
|
|
|
addOtherFile(fileData) {
|
|
|
|
var file = new AudiobookFile()
|
|
|
|
file.setData(fileData)
|
|
|
|
this.otherFiles.push(file)
|
|
|
|
return file
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
update(payload) {
|
|
|
|
var hasUpdates = false
|
|
|
|
|
|
|
|
if (payload.tags && payload.tags.join(',') !== this.tags.join(',')) {
|
|
|
|
this.tags = payload.tags
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.book) {
|
|
|
|
if (!this.book) {
|
|
|
|
this.setBook(payload.book)
|
|
|
|
hasUpdates = true
|
|
|
|
} else if (this.book.update(payload.book)) {
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-24 14:15:56 +02:00
|
|
|
if (hasUpdates) {
|
|
|
|
this.lastUpdate = Date.now()
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
return hasUpdates
|
|
|
|
}
|
|
|
|
|
|
|
|
updateAudioTracks(files) {
|
|
|
|
var index = 1
|
|
|
|
this.audioFiles = files.map((file) => {
|
|
|
|
file.manuallyVerified = true
|
|
|
|
file.invalid = false
|
|
|
|
file.error = null
|
|
|
|
file.index = index++
|
|
|
|
return file
|
|
|
|
})
|
|
|
|
this.tracks = []
|
|
|
|
this.invalidParts = []
|
|
|
|
this.missingParts = []
|
|
|
|
this.audioFiles.forEach((file) => {
|
|
|
|
this.addTrack(file)
|
|
|
|
})
|
2021-08-24 14:15:56 +02:00
|
|
|
this.lastUpdate = Date.now()
|
|
|
|
}
|
|
|
|
|
|
|
|
removeAudioFile(audioFile) {
|
2021-08-26 00:36:54 +02:00
|
|
|
this.tracks = this.tracks.filter(t => t.ino !== audioFile.ino)
|
|
|
|
this.audioFiles = this.audioFiles.filter(f => f.ino !== audioFile.ino)
|
2021-08-24 14:15:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
checkUpdateMissingParts() {
|
2021-08-26 00:36:54 +02:00
|
|
|
var currMissingParts = (this.missingParts || []).join(',') || ''
|
2021-08-24 14:15:56 +02:00
|
|
|
|
|
|
|
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++) {
|
|
|
|
missingParts.push(current_index + x)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
current_index = _track.index + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
this.missingParts = missingParts
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
var newMissingParts = (this.missingParts || []).join(',') || ''
|
|
|
|
var wasUpdated = newMissingParts !== currMissingParts
|
2021-08-24 14:15:56 +02:00
|
|
|
if (wasUpdated && this.missingParts.length) {
|
|
|
|
Logger.info(`[Audiobook] "${this.title}" has ${missingParts.length} missing parts`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return wasUpdated
|
|
|
|
}
|
|
|
|
|
|
|
|
// On scan check other files found with other files saved
|
|
|
|
syncOtherFiles(newOtherFiles) {
|
|
|
|
var currOtherFileNum = this.otherFiles.length
|
|
|
|
|
|
|
|
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
|
|
|
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
2021-08-26 00:36:54 +02:00
|
|
|
|
2021-08-24 14:15:56 +02:00
|
|
|
newOtherFiles.forEach((file) => {
|
|
|
|
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
|
|
|
if (!existingOtherFile) {
|
2021-08-26 00:36:54 +02:00
|
|
|
Logger.debug(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
|
|
|
this.addOtherFile(file)
|
2021-08-24 14:15:56 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
var hasUpdates = currOtherFileNum !== this.otherFiles.length
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
// Check if cover was a local image and that it still exists
|
2021-08-24 14:15:56 +02:00
|
|
|
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
|
|
|
|
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
|
|
|
|
var coverStillExists = imageFiles.find(f => comparePaths(f.path, this.book.cover.substr('/local/'.length)))
|
|
|
|
if (!coverStillExists) {
|
|
|
|
Logger.info(`[Audiobook] Local cover was removed | "${this.title}"`)
|
|
|
|
this.book.cover = null
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
// If no cover set and image file exists then use it
|
2021-08-24 14:15:56 +02:00
|
|
|
if (!this.book.cover && imageFiles.length) {
|
|
|
|
this.book.cover = Path.join('/local', imageFiles[0].path)
|
|
|
|
Logger.info(`[Audiobook] Local cover was set | "${this.title}"`)
|
|
|
|
hasUpdates = true
|
|
|
|
}
|
|
|
|
return hasUpdates
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2021-08-21 23:23:35 +02:00
|
|
|
|
2021-08-26 00:36:54 +02:00
|
|
|
syncAudioFile(audioFile, fileScanData) {
|
|
|
|
var hasUpdates = audioFile.syncFile(fileScanData)
|
|
|
|
if (hasUpdates) {
|
|
|
|
var track = this.tracks.find(t => t.ino === audioFile.ino)
|
|
|
|
if (track) {
|
|
|
|
track.syncFile(fileScanData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hasUpdates
|
|
|
|
}
|
|
|
|
|
|
|
|
syncPaths(audiobookData) {
|
|
|
|
var hasUpdates = false
|
|
|
|
var keysToSync = ['path', 'fullPath']
|
|
|
|
keysToSync.forEach((key) => {
|
|
|
|
if (audiobookData[key] !== undefined && audiobookData[key] !== this[key]) {
|
|
|
|
hasUpdates = true
|
|
|
|
this[key] = audiobookData[key]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (hasUpdates) {
|
|
|
|
this.book.syncPathsUpdated(audiobookData)
|
|
|
|
}
|
|
|
|
return hasUpdates
|
|
|
|
}
|
|
|
|
|
2021-08-21 23:23:35 +02:00
|
|
|
isSearchMatch(search) {
|
|
|
|
return this.book.isSearchMatch(search.toLowerCase().trim())
|
|
|
|
}
|
2021-08-26 00:36:54 +02:00
|
|
|
|
|
|
|
getAudioFileByIno(ino) {
|
|
|
|
return this.audioFiles.find(af => af.ino === ino)
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
module.exports = Audiobook
|