merge upstream

This commit is contained in:
David Leimroth 2022-02-28 18:18:25 +01:00
commit 61bf30e08a
89 changed files with 2963 additions and 1805 deletions

View file

@ -11,6 +11,9 @@ class AudioFile {
this.ext = null
this.path = null
this.fullPath = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.trackNumFromMeta = null
@ -51,6 +54,9 @@ class AudioFile {
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
trackNumFromMeta: this.trackNumFromMeta,
discNumFromMeta: this.discNumFromMeta,
@ -82,6 +88,9 @@ class AudioFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
this.manuallyVerified = !!data.manuallyVerified
this.invalid = !!data.invalid
@ -124,6 +133,9 @@ class AudioFile {
this.ext = fileData.ext
this.path = fileData.path
this.fullPath = fileData.fullPath
this.mtimeMs = fileData.mtimeMs || 0
this.ctimeMs = fileData.ctimeMs || 0
this.birthtimeMs = fileData.birthtimeMs || 0
this.addedAt = Date.now()
this.trackNumFromMeta = fileData.trackNumFromMeta

View file

@ -1,10 +1,11 @@
const Path = require('path')
const fs = require('fs-extra')
const { bytesPretty, readTextFile } = require('../utils/fileUtils')
const { comparePaths, getIno, getId, elapsedPretty } = require('../utils/index')
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
const abmetadataGenerator = require('../utils/abmetadataGenerator')
const Logger = require('../Logger')
const Book = require('./Book')
const AudioTrack = require('./AudioTrack')
@ -21,6 +22,9 @@ class Audiobook {
this.path = null
this.fullPath = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
this.lastUpdate = null
this.lastScan = null
@ -44,6 +48,9 @@ class Audiobook {
if (audiobook) {
this.construct(audiobook)
}
// Temp flags
this.isSavingMetadata = false
}
construct(audiobook) {
@ -53,6 +60,9 @@ class Audiobook {
this.folderId = audiobook.folderId || 'audiobooks'
this.path = audiobook.path
this.fullPath = audiobook.fullPath
this.mtimeMs = audiobook.mtimeMs || 0
this.ctimeMs = audiobook.ctimeMs || 0
this.birthtimeMs = audiobook.birthtimeMs || 0
this.addedAt = audiobook.addedAt
this.lastUpdate = audiobook.lastUpdate || this.addedAt
this.lastScan = audiobook.lastScan || null
@ -173,11 +183,11 @@ class Audiobook {
ino: this.ino,
libraryId: this.libraryId,
folderId: this.folderId,
title: this.title,
author: this.author,
cover: this.cover,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
lastScan: this.lastScan,
@ -204,6 +214,9 @@ class Audiobook {
tags: this.tags,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
duration: this.duration,
@ -227,6 +240,9 @@ class Audiobook {
folderId: this.folderId,
path: this.path,
fullPath: this.fullPath,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt,
lastUpdate: this.lastUpdate,
duration: this.duration,
@ -334,6 +350,9 @@ class Audiobook {
this.path = data.path
this.fullPath = data.fullPath
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = Date.now()
this.lastUpdate = this.addedAt
@ -425,13 +444,8 @@ class Audiobook {
hasUpdates = true
}
if (payload.book) {
if (!this.book) {
this.setBook(payload.book)
hasUpdates = true
} else if (this.book.update(payload.book)) {
hasUpdates = true
}
if (payload.book && this.book.update(payload.book)) {
hasUpdates = true
}
if (hasUpdates) {
@ -526,7 +540,7 @@ class Audiobook {
}
// On scan check other files found with other files saved
async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
var hasUpdates = false
var currOtherFileNum = this.otherFiles.length
@ -535,6 +549,8 @@ class Audiobook {
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
var existingAbMetadata = this.otherFiles.find(file => file.filename === 'metadata.abs')
// Filter out other files no longer in directory
var newOtherFilePaths = newOtherFiles.map(f => f.path)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
@ -543,9 +559,9 @@ class Audiobook {
hasUpdates = true
}
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
// If desc.txt is new then read it and update description (will overwrite)
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
if (descriptionTxt && !alreadyHasDescTxt) {
var newDescription = await readTextFile(descriptionTxt.fullPath)
if (newDescription) {
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
@ -553,9 +569,9 @@ class Audiobook {
hasUpdates = true
}
}
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
// If reader.txt is new then read it and update narrator (will overwrite)
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
if (readerTxt && !alreadyHasReaderTxt) {
var newReader = await readTextFile(readerTxt.fullPath)
if (newReader) {
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
@ -564,7 +580,28 @@ class Audiobook {
}
}
// If OPF file and was not already there
// If metadata.abs is new OR modified then read it and set all defined keys (will overwrite)
var metadataAbs = newOtherFiles.find(file => file.filename === 'metadata.abs')
var shouldUpdateAbs = !!metadataAbs && (metadataAbs.modified || !existingAbMetadata)
if (metadataAbs && metadataAbs.modified) {
Logger.debug(`[Audiobook] metadata.abs file was modified for "${this.title}"`)
}
if (shouldUpdateAbs) {
var abmetadataText = await readTextFile(metadataAbs.fullPath)
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
if (this.update(metadataUpdateObject)) {
Logger.debug(`[Audiobook] Some details were updated from metadata.abs for "${this.title}"`, metadataUpdateObject)
hasUpdates = true
}
}
}
}
// If OPF file and was not already there OR prefer opf metadata
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@ -643,7 +680,7 @@ class Audiobook {
if (bookCoverPath && bookCoverPath.startsWith('/metadata')) {
// Fixing old cover paths
if (!this.book.coverFullPath) {
this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
this.book.coverFullPath = Path.join(global.MetadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
hasUpdates = true
}
@ -800,9 +837,10 @@ class Audiobook {
return false
}
// Look for desc.txt and reader.txt and update details if found
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
var bookUpdatePayload = {}
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
if (descriptionText) {
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
@ -814,6 +852,22 @@ class Audiobook {
bookUpdatePayload.narrator = readerText
}
// abmetadata will always overwrite
var abmetadataText = await this.fetchTextFromTextFile('metadata.abs')
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
Logger.debug(`[Audiobook] "${this.title}" found metadata.abs file`)
for (const key in metadataUpdateObject.book) {
var value = metadataUpdateObject.book[key]
if (key && value !== undefined) {
bookUpdatePayload[key] = value
}
}
}
}
// Opf only overwrites if detail is empty
var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@ -873,12 +927,6 @@ class Audiobook {
}
}
if (existingFile.filename !== fileFound.filename) {
existingFile.filename = fileFound.filename
existingFile.ext = fileFound.ext
hasUpdated = true
}
if (existingFile.path !== fileFound.path) {
existingFile.path = fileFound.path
existingFile.fullPath = fileFound.fullPath
@ -888,6 +936,20 @@ class Audiobook {
hasUpdated = true
}
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
keysToCheck.forEach((key) => {
if (existingFile[key] !== fileFound[key]) {
// Add modified flag on file data object if exists and was changed
if (key === 'mtimeMs' && existingFile[key]) {
fileFound.modified = true
}
existingFile[key] = fileFound[key]
hasUpdated = true
}
})
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
existingFile.filetype = fileFound.filetype
hasUpdated = true
@ -927,6 +989,14 @@ class Audiobook {
hasUpdated = true
}
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
keysToCheck.forEach((key) => {
if (dataFound[key] != this[key]) {
this[key] = dataFound[key] || 0
hasUpdated = true
}
})
var newAudioFileData = []
var newOtherFileData = []
var existingAudioFileData = []
@ -1017,14 +1087,14 @@ class Audiobook {
}
// Temp fix for cover is set but coverFullPath is not set
fixFullCoverPath(metadataPath) {
fixFullCoverPath() {
if (!this.book.cover) return
var bookCoverPath = this.book.cover.replace(/\\/g, '/')
var newFullCoverPath = null
if (bookCoverPath.startsWith('/s/book/')) {
newFullCoverPath = Path.join(this.fullPath, bookCoverPath.substr(`/s/book/${this.id}`.length)).replace(/\/\//g, '/')
} else if (bookCoverPath.startsWith('/metadata/')) {
newFullCoverPath = Path.join(metadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
newFullCoverPath = Path.join(global.MetadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
}
if (newFullCoverPath) {
Logger.debug(`[Audiobook] "${this.title}" fixing full cover path "${this.book.cover}" => "${newFullCoverPath}"`)
@ -1033,5 +1103,26 @@ class Audiobook {
}
return false
}
async saveAbMetadata() {
if (this.isSavingMetadata) return
this.isSavingMetadata = true
var metadataPath = Path.join(global.MetadataPath, 'books', this.id)
if (global.ServerSettings.storeMetadataWithBook) {
metadataPath = this.fullPath
} else {
// Make sure metadata book dir exists
await fs.ensureDir(metadataPath)
}
metadataPath = Path.join(metadataPath, 'metadata.abs')
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
this.isSavingMetadata = false
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
return success
})
}
}
module.exports = Audiobook

View file

@ -6,6 +6,11 @@ class AudiobookFile {
this.ext = null
this.path = null
this.fullPath = null
this.size = null
this.mtimeMs = null
this.ctimeMs = null
this.birthtimeMs = null
this.addedAt = null
if (data) {
@ -25,6 +30,10 @@ class AudiobookFile {
ext: this.ext,
path: this.path,
fullPath: this.fullPath,
size: this.size,
mtimeMs: this.mtimeMs,
ctimeMs: this.ctimeMs,
birthtimeMs: this.birthtimeMs,
addedAt: this.addedAt
}
}
@ -36,6 +45,10 @@ class AudiobookFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = data.addedAt
}
@ -46,6 +59,10 @@ class AudiobookFile {
this.ext = data.ext
this.path = data.path
this.fullPath = data.fullPath
this.size = data.size || 0
this.mtimeMs = data.mtimeMs || 0
this.ctimeMs = data.ctimeMs || 0
this.birthtimeMs = data.birthtimeMs || 0
this.addedAt = Date.now()
}
}

View file

@ -49,6 +49,20 @@ class Book {
get _asin() { return this.asin || '' }
get genresCommaSeparated() { return this._genres.join(', ') }
get titleIgnorePrefix() {
if (this._title.toLowerCase().startsWith('the ')) {
return this._title.substr(4) + ', The'
}
return this._title
}
get seriesIgnorePrefix() {
if (this._series.toLowerCase().startsWith('the ')) {
return this._series.substr(4) + ', The'
}
return this._series
}
get shouldSearchForCover() {
if (this.cover) return false
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true

View file

@ -8,6 +8,8 @@ class Library {
this.folders = []
this.displayOrder = 1
this.icon = 'database'
this.provider = 'google'
this.disableWatcher = false
this.lastScan = 0
@ -29,6 +31,8 @@ class Library {
this.folders = (library.folders || []).map(f => new Folder(f))
this.displayOrder = library.displayOrder || 1
this.icon = library.icon || 'database'
this.provider = library.provider || 'google'
this.disableWatcher = !!library.disableWatcher
this.createdAt = library.createdAt
this.lastUpdate = library.lastUpdate
@ -41,6 +45,8 @@ class Library {
folders: (this.folders || []).map(f => f.toJSON()),
displayOrder: this.displayOrder,
icon: this.icon,
provider: this.provider,
disableWatcher: this.disableWatcher,
createdAt: this.createdAt,
lastUpdate: this.lastUpdate
}
@ -65,6 +71,7 @@ class Library {
}
this.displayOrder = data.displayOrder || 1
this.icon = data.icon || 'database'
this.disableWatcher = !!data.disableWatcher
this.createdAt = Date.now()
this.lastUpdate = Date.now()
}
@ -75,6 +82,14 @@ class Library {
this.name = payload.name
hasUpdates = true
}
if (payload.provider && payload.provider !== this.provider) {
this.provider = payload.provider
hasUpdates = true
}
if (payload.disableWatcher !== this.disableWatcher) {
this.disableWatcher = !!payload.disableWatcher
hasUpdates = true
}
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
this.displayOrder = Number(payload.displayOrder)
hasUpdates = true

View file

@ -1,4 +1,4 @@
const { CoverDestination, BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
const { BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
const Logger = require('../Logger')
class ServerSettings {
@ -15,10 +15,11 @@ class ServerSettings {
this.scannerCoverProvider = 'google'
this.scannerPreferAudioMetadata = false
this.scannerPreferOpfMetadata = false
this.scannerDisableWatcher = false
// Metadata
this.coverDestination = CoverDestination.METADATA
this.saveMetadataFile = false
this.storeCoverWithBook = false
this.storeMetadataWithBook = false
// Security/Rate limits
this.rateLimitLoginRequests = 10
@ -38,6 +39,8 @@ class ServerSettings {
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
this.bookshelfView = BookshelfView.STANDARD
this.sortingIgnorePrefix = false
this.chromecastEnabled = false
this.logLevel = Logger.logLevel
this.version = null
@ -54,9 +57,14 @@ class ServerSettings {
this.scannerParseSubtitle = settings.scannerParseSubtitle
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
this.storeCoverWithBook = settings.storeCoverWithBook
if (this.storeCoverWithBook == undefined) { // storeCoverWithBook added in 1.7.1 to replace coverDestination
this.storeCoverWithBook = !!settings.coverDestination
}
this.storeMetadataWithBook = !!settings.storeCoverWithBook
this.coverDestination = settings.coverDestination || CoverDestination.METADATA
this.saveMetadataFile = !!settings.saveMetadataFile
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
@ -70,6 +78,8 @@ class ServerSettings {
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
this.chromecastEnabled = !!settings.chromecastEnabled
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
@ -88,8 +98,9 @@ class ServerSettings {
scannerParseSubtitle: this.scannerParseSubtitle,
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
coverDestination: this.coverDestination,
saveMetadataFile: !!this.saveMetadataFile,
scannerDisableWatcher: this.scannerDisableWatcher,
storeCoverWithBook: this.storeCoverWithBook,
storeMetadataWithBook: this.storeMetadataWithBook,
rateLimitLoginRequests: this.rateLimitLoginRequests,
rateLimitLoginWindow: this.rateLimitLoginWindow,
backupSchedule: this.backupSchedule,
@ -99,6 +110,8 @@ class ServerSettings {
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
coverAspectRatio: this.coverAspectRatio,
bookshelfView: this.bookshelfView,
sortingIgnorePrefix: this.sortingIgnorePrefix,
chromecastEnabled: this.chromecastEnabled,
logLevel: this.logLevel,
version: this.version
}

View file

@ -175,11 +175,6 @@ class Stream extends EventEmitter {
return false
}
updateClientCurrentTime(currentTime) {
Logger.debug('[Stream] Updated client current time', secondsToTimestamp(currentTime))
this.clientCurrentTime = currentTime
}
syncStream({ timeListened, currentTime }) {
var syncLog = ''
// Set user current time