From c60807f9988c676c63f4569d8dbebdfc5e222383 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 10 Apr 2022 10:05:05 -0500 Subject: [PATCH] Removing remaining legacy objects, remove njodb error for fileExists --- client/package.json | 2 +- package.json | 4 +- server/Server.js | 2 +- server/finders/AuthorFinder.js | 76 -- server/njodb/utils.js | 4 +- server/objects/legacy/AudioFile.js | 243 ---- server/objects/legacy/AudioTrack.js | 116 -- server/objects/legacy/Audiobook.js | 1128 ----------------- server/objects/legacy/AudiobookFile.js | 69 - server/objects/legacy/Author.js | 72 -- server/objects/legacy/Book.js | 417 ------ server/objects/legacy/StreamManager.js | 231 ---- server/objects/legacy/UserAudiobookData.js | 136 -- server/objects/legacy/UserListeningSession.js | 98 -- server/utils/dbMigration.js | 22 +- 15 files changed, 18 insertions(+), 2602 deletions(-) delete mode 100644 server/objects/legacy/AudioFile.js delete mode 100644 server/objects/legacy/AudioTrack.js delete mode 100644 server/objects/legacy/Audiobook.js delete mode 100644 server/objects/legacy/AudiobookFile.js delete mode 100644 server/objects/legacy/Author.js delete mode 100644 server/objects/legacy/Book.js delete mode 100644 server/objects/legacy/StreamManager.js delete mode 100644 server/objects/legacy/UserAudiobookData.js delete mode 100644 server/objects/legacy/UserListeningSession.js diff --git a/client/package.json b/client/package.json index dfc25df6..3052478c 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.7.2", + "version": "1.7.3", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/package.json b/package.json index e2db3939..95829944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.7.2", + "version": "1.7.3", "description": "Self-hosted audiobook server for managing and playing audiobooks", "main": "index.js", "scripts": { @@ -53,4 +53,4 @@ "xml2js": "^0.4.23" }, "devDependencies": {} -} +} \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index a7fac9b0..380cacc3 100644 --- a/server/Server.js +++ b/server/Server.js @@ -117,7 +117,7 @@ class Server { // await this.streamManager.removeOrphanStreams() await this.downloadManager.removeOrphanDownloads() - if (version.localeCompare('1.7.3') < 0) { // Old version data model migration + if (version.localeCompare('2.0.0') < 0) { // Old version data model migration await dbMigration.migrate(this.db) } else { await this.db.init() diff --git a/server/finders/AuthorFinder.js b/server/finders/AuthorFinder.js index 9504b698..893ed2b5 100644 --- a/server/finders/AuthorFinder.js +++ b/server/finders/AuthorFinder.js @@ -1,7 +1,6 @@ const fs = require('fs-extra') const Logger = require('../Logger') const Path = require('path') -const Author = require('../objects/legacy/Author') const Audnexus = require('../providers/Audnexus') const { downloadFile } = require('../utils/fileUtils') @@ -51,80 +50,5 @@ class AuthorFinder { relPath } } - - async createAuthor(payload) { - if (!payload || !payload.name) return null - - var authorDir = Path.posix.join(this.AuthorPath, payload.name) - var relAuthorDir = Path.posix.join('/metadata', 'authors', payload.name) - - if (payload.image && payload.image.startsWith('http')) { - await fs.ensureDir(authorDir) - - var imageExtension = payload.image.toLowerCase().split('.').pop() - var ext = imageExtension === 'png' ? 'png' : 'jpg' - var filename = 'photo.' + ext - var outputPath = Path.posix.join(authorDir, filename) - var relPath = Path.posix.join(relAuthorDir, filename) - - var success = await this.downloadImage(payload.image, outputPath) - if (!success) { - await fs.rmdir(authorDir).catch((error) => { - Logger.error(`[AuthorFinder] Failed to remove author dir`, authorDir, error) - }) - payload.image = null - payload.imageFullPath = null - } else { - payload.image = relPath - payload.imageFullPath = outputPath - } - } else { - payload.image = null - payload.imageFullPath = null - } - - var author = new Author() - author.setData(payload) - - return author - } - - async getAuthorByName(name, options = {}) { - var authorData = await this.findAuthorByName(name, options) - if (!authorData) return null - - var authorDir = Path.posix.join(this.AuthorPath, authorData.name) - var relAuthorDir = Path.posix.join('/metadata', 'authors', authorData.name) - - if (authorData.image) { - await fs.ensureDir(authorDir) - - var imageExtension = authorData.image.toLowerCase().split('.').pop() - var ext = imageExtension === 'png' ? 'png' : 'jpg' - var filename = 'photo.' + ext - var outputPath = Path.posix.join(authorDir, filename) - var relPath = Path.posix.join(relAuthorDir, filename) - - var success = await this.downloadImage(authorData.image, outputPath) - if (!success) { - await fs.rmdir(authorDir).catch((error) => { - Logger.error(`[AuthorFinder] Failed to remove author dir`, authorDir, error) - }) - authorData.image = null - authorData.imageFullPath = null - } else { - authorData.image = relPath - authorData.imageFullPath = outputPath - } - } else { - authorData.image = null - authorData.imageFullPath = null - } - - var author = new Author() - author.setData(authorData) - - return author - } } module.exports = AuthorFinder \ No newline at end of file diff --git a/server/njodb/utils.js b/server/njodb/utils.js index f025f495..2c0e7cbd 100644 --- a/server/njodb/utils.js +++ b/server/njodb/utils.js @@ -27,7 +27,7 @@ const max = (a, b) => { const convertSize = (size) => { const sizes = ["bytes", "KB", "MB", "GB"]; - var index = Math.floor(Math.log2(size)/10); + var index = Math.floor(Math.log2(size) / 10); if (index > 3) index = 3; return Math.round(((size / Math.pow(1024, index)) + Number.EPSILON) * 100) / 100 + " " + sizes[index]; @@ -38,7 +38,7 @@ const fileExists = async (a) => { await promisify(access)(a, constants.F_OK); return true; } catch (error) { - console.error(error); + // console.error(error); file does not exist no need for error return false; } } diff --git a/server/objects/legacy/AudioFile.js b/server/objects/legacy/AudioFile.js deleted file mode 100644 index 25dbf495..00000000 --- a/server/objects/legacy/AudioFile.js +++ /dev/null @@ -1,243 +0,0 @@ -const { isNullOrNaN } = require('../../utils/index') -const AudioFileMetadata = require('../metadata/AudioMetaTags') - -class AudioFile { - constructor(data) { - this.index = null - this.ino = null - this.filename = null - this.ext = null - this.path = null - this.fullPath = null - this.mtimeMs = null - this.ctimeMs = null - this.birthtimeMs = null - this.addedAt = null - - this.trackNumFromMeta = null - this.discNumFromMeta = null - this.trackNumFromFilename = null - this.discNumFromFilename = null - - this.format = null - this.duration = null - this.size = null - this.bitRate = null - this.language = null - this.codec = null - this.timeBase = null - this.channels = null - this.channelLayout = null - this.chapters = [] - this.embeddedCoverArt = null - - // Tags scraped from the audio file - this.metadata = null - - this.manuallyVerified = false - this.invalid = false - this.exclude = false - this.error = null - - if (data) { - this.construct(data) - } - } - - toJSON() { - return { - index: this.index, - ino: this.ino, - filename: this.filename, - 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, - trackNumFromFilename: this.trackNumFromFilename, - discNumFromFilename: this.discNumFromFilename, - manuallyVerified: !!this.manuallyVerified, - invalid: !!this.invalid, - exclude: !!this.exclude, - error: this.error || null, - format: this.format, - duration: this.duration, - size: this.size, - bitRate: this.bitRate, - language: this.language, - codec: this.codec, - timeBase: this.timeBase, - channels: this.channels, - channelLayout: this.channelLayout, - chapters: this.chapters, - embeddedCoverArt: this.embeddedCoverArt, - metadata: this.metadata ? this.metadata.toJSON() : {} - } - } - - construct(data) { - this.index = data.index - this.ino = data.ino - this.filename = data.filename - 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 - this.exclude = !!data.exclude - this.error = data.error || null - - this.trackNumFromMeta = data.trackNumFromMeta - this.discNumFromMeta = data.discNumFromMeta - this.trackNumFromFilename = data.trackNumFromFilename - - if (data.cdNumFromFilename !== undefined) this.discNumFromFilename = data.cdNumFromFilename // TEMP:Support old var name - else this.discNumFromFilename = data.discNumFromFilename - - this.format = data.format - this.duration = data.duration - this.size = data.size - this.bitRate = data.bitRate - this.language = data.language - this.codec = data.codec || null - this.timeBase = data.timeBase - this.channels = data.channels - this.channelLayout = data.channelLayout - this.chapters = data.chapters - this.embeddedCoverArt = data.embeddedCoverArt || null - - // 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 || {}) - } - } - - // New scanner creates AudioFile from AudioFileScanner - setDataFromProbe(fileData, probeData) { - this.index = fileData.index || null - this.ino = fileData.ino || null - this.filename = fileData.filename - 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 - this.discNumFromMeta = fileData.discNumFromMeta - this.trackNumFromFilename = fileData.trackNumFromFilename - this.discNumFromFilename = fileData.discNumFromFilename - - this.format = probeData.format - this.duration = probeData.duration - this.size = probeData.size - this.bitRate = probeData.bitRate || null - this.language = probeData.language - this.codec = probeData.codec || null - this.timeBase = probeData.timeBase - this.channels = probeData.channels - this.channelLayout = probeData.channelLayout - this.chapters = probeData.chapters || [] - this.metadata = probeData.audioFileMetadata - this.embeddedCoverArt = probeData.embeddedCoverArt - } - - validateTrackIndex() { - var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta) - var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename) - - if (numFromMeta !== null) return numFromMeta - if (numFromFilename !== null) return numFromFilename - - this.invalid = true - this.error = 'Failed to get track number' - return null - } - - setDuplicateTrackNumber(num) { - this.invalid = true - this.error = 'Duplicate track number "' + num + '"' - } - - syncChapters(updatedChapters) { - if (this.chapters.length !== updatedChapters.length) { - this.chapters = updatedChapters.map(ch => ({ ...ch })) - return true - } else if (updatedChapters.length === 0) { - if (this.chapters.length > 0) { - this.chapters = [] - return true - } - return false - } - - var hasUpdates = false - for (let i = 0; i < updatedChapters.length; i++) { - if (JSON.stringify(updatedChapters[i]) !== JSON.stringify(this.chapters[i])) { - hasUpdates = true - } - } - if (hasUpdates) { - this.chapters = updatedChapters.map(ch => ({ ...ch })) - } - return hasUpdates - } - - clone() { - return new AudioFile(this.toJSON()) - } - - // If the file or parent directory was renamed it is synced here - syncFile(newFile) { - var hasUpdates = false - var keysToSync = ['path', 'fullPath', 'ext', 'filename'] - keysToSync.forEach((key) => { - if (newFile[key] !== undefined && newFile[key] !== this[key]) { - hasUpdates = true - this[key] = newFile[key] - } - }) - return hasUpdates - } - - updateFromScan(scannedAudioFile) { - var hasUpdated = false - - var newjson = scannedAudioFile.toJSON() - if (this.manuallyVerified) newjson.manuallyVerified = true - if (this.exclude) newjson.exclude = true - newjson.addedAt = this.addedAt - - for (const key in newjson) { - if (key === 'metadata') { - if (!this.metadata || !this.metadata.isEqual(scannedAudioFile.metadata)) { - this.metadata = scannedAudioFile.metadata - hasUpdated = true - } - } else if (key === 'chapters') { - if (this.syncChapters(newjson.chapters || [])) { - hasUpdated = true - } - } else if (this[key] !== newjson[key]) { - // console.log(this.filename, 'key', key, 'updated', this[key], newjson[key]) - this[key] = newjson[key] - hasUpdated = true - } - } - return hasUpdated - } -} -module.exports = AudioFile \ No newline at end of file diff --git a/server/objects/legacy/AudioTrack.js b/server/objects/legacy/AudioTrack.js deleted file mode 100644 index 656d5eed..00000000 --- a/server/objects/legacy/AudioTrack.js +++ /dev/null @@ -1,116 +0,0 @@ -var { bytesPretty } = require('../../utils/fileUtils') - -class AudioTrack { - constructor(audioTrack = null) { - this.index = null - this.ino = null - - this.path = null - this.fullPath = null - this.ext = null - this.filename = null - - this.format = null - this.duration = null - this.size = null - this.bitRate = null - this.language = null - this.codec = null - this.timeBase = null - this.channels = null - this.channelLayout = null - - if (audioTrack) { - this.construct(audioTrack) - } - } - - construct(audioTrack) { - this.index = audioTrack.index - this.ino = audioTrack.ino || null - - this.path = audioTrack.path - this.fullPath = audioTrack.fullPath - this.ext = audioTrack.ext - this.filename = audioTrack.filename - - this.format = audioTrack.format - this.duration = audioTrack.duration - this.size = audioTrack.size - this.bitRate = audioTrack.bitRate - this.language = audioTrack.language - this.codec = audioTrack.codec - this.timeBase = audioTrack.timeBase - this.channels = audioTrack.channels - this.channelLayout = audioTrack.channelLayout - } - - get name() { - return `${String(this.index).padStart(3, '0')}: ${this.filename} (${bytesPretty(this.size)}) [${this.duration}]` - } - - toJSON() { - return { - index: this.index, - ino: this.ino, - path: this.path, - fullPath: this.fullPath, - ext: this.ext, - filename: this.filename, - format: this.format, - duration: this.duration, - size: this.size, - bitRate: this.bitRate, - language: this.language, - codec: this.codec, - timeBase: this.timeBase, - channels: this.channels, - channelLayout: this.channelLayout, - } - } - - setData(probeData) { - this.index = probeData.index - this.ino = probeData.ino || null - - this.path = probeData.path - this.fullPath = probeData.fullPath - this.ext = probeData.ext - this.filename = probeData.filename - - this.format = probeData.format - this.duration = probeData.duration - this.size = probeData.size - this.bitRate = probeData.bitRate - this.language = probeData.language - this.codec = probeData.codec || null - this.timeBase = probeData.timeBase - this.channels = probeData.channels - this.channelLayout = probeData.channelLayout - } - - syncMetadata(audioFile) { - var hasUpdates = false - var keysToSync = ['format', 'duration', 'size', 'bitRate', 'language', 'codec', 'timeBase', 'channels', 'channelLayout'] - keysToSync.forEach((key) => { - if (audioFile[key] !== undefined && audioFile[key] !== this[key]) { - hasUpdates = true - this[key] = audioFile[key] - } - }) - return hasUpdates - } - - syncFile(newFile) { - var hasUpdates = false - var keysToSync = ['path', 'fullPath', 'ext', 'filename'] - keysToSync.forEach((key) => { - if (newFile[key] !== undefined && newFile[key] !== this[key]) { - hasUpdates = true - this[key] = newFile[key] - } - }) - return hasUpdates - } -} -module.exports = AudioTrack \ No newline at end of file diff --git a/server/objects/legacy/Audiobook.js b/server/objects/legacy/Audiobook.js deleted file mode 100644 index 05aa70bc..00000000 --- a/server/objects/legacy/Audiobook.js +++ /dev/null @@ -1,1128 +0,0 @@ -const Path = require('path') -const fs = require('fs-extra') -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') -const AudioFile = require('./AudioFile') -const AudiobookFile = require('./AudiobookFile') - -class Audiobook { - constructor(audiobook = null) { - this.id = null - this.ino = null // Inode - - this.libraryId = null - this.folderId = null - - this.path = null - this.fullPath = null - this.mtimeMs = null - this.ctimeMs = null - this.birthtimeMs = null - this.addedAt = null - this.lastUpdate = null - this.lastScan = null - this.scanVersion = null - - this.tracks = [] - this.missingParts = [] - - this.audioFiles = [] - this.otherFiles = [] - - this.tags = [] - this.book = null - this.chapters = [] - - // Audiobook was scanned and not found - this.isMissing = false - // Audiobook no longer has "book" files - this.isInvalid = false - - if (audiobook) { - this.construct(audiobook) - } - - // Temp flags - this.isSavingMetadata = false - } - - construct(audiobook) { - this.id = audiobook.id - this.ino = audiobook.ino || null - this.libraryId = audiobook.libraryId || 'main' - 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 - this.scanVersion = audiobook.scanVersion || null - - this.tracks = audiobook.tracks.map(track => new AudioTrack(track)) - this.missingParts = audiobook.missingParts - - this.audioFiles = audiobook.audioFiles.map(file => new AudioFile(file)) - this.otherFiles = audiobook.otherFiles.map(file => new AudiobookFile(file)) - - this.tags = audiobook.tags - if (audiobook.book) { - this.book = new Book(audiobook.book) - } - if (audiobook.chapters) { - this.chapters = audiobook.chapters.map(c => ({ ...c })) - } - - this.isMissing = !!audiobook.isMissing - this.isInvalid = !!audiobook.isInvalid - } - - get title() { - return this.book ? this.book.title : 'No Title' - } - - get author() { - return this.book ? this.book.author : 'Unknown' - } - - get cover() { - return this.book ? this.book.cover : '' - } - - get authorLF() { - return this.book ? this.book.authorLF : null - } - - get authorFL() { - return this.book ? this.book.authorFL : null - } - - get genres() { - return this.book ? this.book.genres || [] : [] - } - - get duration() { - var total = 0 - this.tracks.forEach((track) => total += track.duration) - return total - } - - get size() { - var total = 0 - this.tracks.forEach((track) => total += track.size) - return total - } - - get sizePretty() { - return bytesPretty(this.size) - } - - get durationPretty() { - return elapsedPretty(this.duration) - } - - get invalidParts() { - return this._audioFiles.filter(af => af.invalid).map(af => ({ filename: af.filename, error: af.error || 'Unknown Error' })) - } - - get numMissingParts() { - return this.missingParts ? this.missingParts.length : 0 - } - - get numInvalidParts() { - return this.invalidParts ? this.invalidParts.length : 0 - } - - get _audioFiles() { return this.audioFiles || [] } - get _otherFiles() { return this.otherFiles || [] } - get _tracks() { return this.tracks || [] } - - get audioFilesToInclude() { return this._audioFiles.filter(af => !af.exclude) } - - get ebooks() { - return this.otherFiles.filter(file => file.filetype === 'ebook') - } - - get hasMissingIno() { - 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) - } - - // TEMP: Issue with inodes not always being set for files - getFilesWithMissingIno() { - var afs = this._audioFiles.filter(af => !af.ino) - var ofs = this._otherFiles.filter(f => !f.ino) - var ts = this._tracks.filter(t => !t.ino) - return afs.concat(ofs).concat(ts) - } - - 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, - ino: this.ino, - libraryId: this.libraryId, - folderId: this.folderId, - path: this.path, - fullPath: this.fullPath, - mtimeMs: this.mtimeMs, - ctimeMs: this.ctimeMs, - birthtimeMs: this.birthtimeMs, - addedAt: this.addedAt, - lastUpdate: this.lastUpdate, - lastScan: this.lastScan, - scanVersion: this.scanVersion, - missingParts: this.missingParts, - tags: this.tags, - book: this.bookToJSON(), - tracks: this.tracksToJSON(), - audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), - otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), - chapters: this.chapters || [], - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid - } - } - - toJSONMinified() { - return { - id: this.id, - ino: this.ino, - libraryId: this.libraryId, - folderId: this.folderId, - book: this.bookToJSON(), - 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, - size: this.size, - ebooks: this.ebooks.map(ebook => ebook.toJSON()), - numEbooks: this.ebooks.length, - numTracks: this.tracks.length, - numChapters: (this.chapters || []).length, - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid, - hasMissingParts: this.numMissingParts, - hasInvalidParts: this.numInvalidParts - } - } - - toJSONExpanded() { - return { - id: this.id, - ino: this.ino, - libraryId: this.libraryId, - 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, - durationPretty: this.durationPretty, - size: this.size, - sizePretty: this.sizePretty, - missingParts: this.missingParts, - invalidParts: this.invalidParts, - audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), - otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), - ebooks: this.ebooks.map(ebook => ebook.toJSON()), - numEbooks: this.ebooks.length, - numTracks: this.tracks.length, - tags: this.tags, - book: this.bookToJSON(), - tracks: this.tracksToJSON(), - chapters: this.chapters || [], - isMissing: !!this.isMissing, - isInvalid: !!this.isInvalid, - hasMissingParts: this.numMissingParts, - hasInvalidParts: this.numInvalidParts - } - } - - // Originally files did not store the inode value - // this function checks all files and sets the inode - async checkUpdateInos() { - var hasUpdates = false - - // Audiobook folder needs inode - if (!this.ino) { - this.ino = await getIno(this.fullPath) - hasUpdates = true - } - - // Check audio files have an inode - for (let i = 0; i < this.audioFiles.length; i++) { - var af = this.audioFiles[i] - var at = this.tracks.find(t => t.ino === af.ino) - if (!at) { - at = this.tracks.find(t => comparePaths(t.path, af.path)) - if (!at && !af.exclude) { - Logger.warn(`[Audiobook] No matching track for audio file "${af.filename}"`) - } - } - 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 { - Logger.debug(`[Audiobook] Set INO For audio file ${af.path}`) - if (at) at.ino = af.ino - } - hasUpdates = true - } else if (at && at.ino !== af.ino) { - at.ino = af.ino - hasUpdates = true - } - } - - for (let i = 0; i < this.tracks.length; i++) { - var at = this.tracks[i] - if (!at.ino) { - Logger.debug(`[Audiobook] Track ${at.filename} still does not have ino`) - var atino = await getIno(at.fullPath) - var af = this.audioFiles.find(_af => _af.ino === atino) - if (!af) { - Logger.debug(`[Audiobook] Track ${at.filename} no matching audio file with ino ${atino}`) - af = this.audioFiles.find(_af => _af.filename === at.filename) - if (!af) { - Logger.debug(`[Audiobook] Track ${at.filename} no matching audio file with filename`) - } else { - Logger.debug(`[Audiobook] Track ${at.filename} found matching filename but mismatch ino ${atino}/${af.ino}`) - // at.ino = af.ino - // at.path = af.path - // at.fullPath = af.fullPath - // hasUpdates = true - } - } else { - Logger.debug(`[Audiobook] Track ${at.filename} found audio file with matching ino ${at.path}/${af.path}`) - } - } - } - - for (let i = 0; i < this.otherFiles.length; i++) { - var file = this.otherFiles[i] - if (!file.ino || file.ino === this.ino) { - file.ino = await getIno(file.fullPath) - if (!file.ino) { - Logger.error('[Audiobook] checkUpdateInos: Failed to set ino for other file', file.fullPath) - } else { - Logger.debug(`[Audiobook] Set INO For other file ${file.path}`) - } - hasUpdates = true - } - } - return hasUpdates - } - - setData(data) { - this.id = getId('ab') - this.libraryId = data.libraryId || 'main' - this.folderId = data.folderId || 'audiobooks' - this.ino = data.ino || null - - 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 - - if (data.otherFiles) { - data.otherFiles.forEach((file) => { - this.addOtherFile(file) - }) - } - - this.setBook(data) - } - - checkHasOldCoverPath() { - return this.book.cover && !this.book.coverFullPath - } - - setLastScan(version) { - this.lastScan = Date.now() - this.lastUpdate = Date.now() - this.scanVersion = version - } - - setMissing() { - this.isMissing = true - this.lastUpdate = Date.now() - } - - setInvalid() { - this.isInvalid = true - this.lastUpdate = Date.now() - } - - setBook(data) { - // Use first image file as cover - if (this.otherFiles && this.otherFiles.length) { - var imageFile = this.otherFiles.find(f => f.filetype === 'image') - if (imageFile) { - data.coverFullPath = imageFile.fullPath - var relImagePath = imageFile.path.replace(this.path, '') - data.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath) - } - } - - this.book = new Book() - this.book.setData(data) - } - - setCoverFromFile(file) { - if (!file || !file.fullPath || !file.path) { - Logger.error(`[Audiobook] "${this.title}" Invalid file for setCoverFromFile`, file) - return false - } - var updateBookPayload = {} - updateBookPayload.coverFullPath = file.fullPath - // Set ab local static path from file relative path - var relImagePath = file.path.replace(this.path, '') - updateBookPayload.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath) - return this.book.update(updateBookPayload) - } - - addTrack(trackData) { - var track = new AudioTrack() - track.setData(trackData) - this.tracks.push(track) - return track - } - - addAudioFile(audioFileData) { - this.audioFiles.push(audioFileData) - return audioFileData - } - - updateAudioFile(updatedAudioFile) { - var audioFile = this.audioFiles.find(af => af.ino === updatedAudioFile.ino) - return audioFile.updateFromScan(updatedAudioFile) - } - - addOtherFile(fileData) { - var file = new AudiobookFile() - file.setData(fileData) - this.otherFiles.push(file) - return file - } - - update(payload) { - var hasUpdates = false - if (payload.tags && payload.tags.join(',') !== this.tags.join(',')) { - this.tags = payload.tags - hasUpdates = true - } - - if (payload.book && this.book.update(payload.book)) { - hasUpdates = true - } - - if (hasUpdates) { - this.lastUpdate = Date.now() - } - - return hasUpdates - } - - // Cover Url may be the same, this ensures the lastUpdate is updated - updateBookCover(cover, coverFullPath) { - if (!this.book) return false - return this.book.updateCover(cover, coverFullPath) - } - - checkHasTrackNum(trackNum, excludeIno) { - return this._audioFiles.find(t => t.index === trackNum && t.ino !== excludeIno) - } - - 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() - } - - // After audio files have been added/removed/updated this method sets tracks - rebuildTracks() { - this.audioFiles.sort((a, b) => a.index - b.index) - this.tracks = [] - this.missingParts = [] - this.audioFiles.forEach((file) => { - if (!file.exclude) { - this.addTrack(file) - } - }) - this.setChapters() - this.checkUpdateMissingTracks() - this.lastUpdate = Date.now() - } - - removeAudioFile(audioFile) { - this.tracks = this.tracks.filter(t => t.ino !== audioFile.ino) - this.audioFiles = this.audioFiles.filter(f => f.ino !== audioFile.ino) - } - - removeAudioTrack(track) { - this.tracks = this.tracks.filter(t => t.ino !== track.ino) - this.audioFiles = this.audioFiles.filter(f => f.ino !== track.ino) - } - - 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.title}" has ${missingParts.length} missing parts`) - } - - return wasUpdated - } - - // On scan check other files found with other files saved - async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) { - var hasUpdates = false - - var currOtherFileNum = this.otherFiles.length - - var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename) - 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)) - if (currOtherFileNum !== this.otherFiles.length) { - Logger.debug(`[Audiobook] ${currOtherFileNum - this.otherFiles.length} other files were removed for "${this.title}"`) - hasUpdates = true - } - - // 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) { - var newDescription = await readTextFile(descriptionTxt.fullPath) - if (newDescription) { - Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`) - this.update({ book: { description: newDescription } }) - hasUpdates = true - } - } - // 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) { - var newReader = await readTextFile(readerTxt.fullPath) - if (newReader) { - Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`) - this.update({ book: { narrator: newReader } }) - hasUpdates = true - } - } - - - // 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) - if (xmlText) { - var opfMetadata = await parseOpfMetadataXML(xmlText) - // Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata) - if (opfMetadata) { - const bookUpdatePayload = {} - for (const key in opfMetadata) { - // Add genres only if genres are empty - if (key === 'genres') { - if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) { - bookUpdatePayload[key] = opfMetadata.genres - } - } else if (opfMetadata[key] && (!this.book[key] || opfMetadataOverrideDetails)) { - bookUpdatePayload[key] = opfMetadata[key] - } - } - if (Object.keys(bookUpdatePayload).length) { - Logger.debug(`[Audiobook] Using data found in OPF "${metadataOpf.filename}"`, bookUpdatePayload) - this.update({ book: bookUpdatePayload }) - hasUpdates = true - } - } - } - } - - newOtherFiles.forEach((file) => { - var existingOtherFile = this.otherFiles.find(f => f.ino === file.ino) - if (!existingOtherFile) { - Logger.debug(`[Audiobook] New other file found on sync ${file.filename} | "${this.title}"`) - this.addOtherFile(file) - hasUpdates = true - } - }) - - var imageFiles = this.otherFiles.filter(f => f.filetype === 'image') - - // OLD Path Check if cover was a local image and that it still exists - if (this.book.cover && this.book.cover.substr(1).startsWith('local')) { - var coverStripped = this.book.cover.substr('/local/'.length) - // Check if was removed first - var coverStillExists = imageFiles.find(f => comparePaths(f.path, coverStripped)) - if (!coverStillExists) { - Logger.info(`[Audiobook] Local cover was removed | "${this.title}"`) - this.book.removeCover() - } else { - var oldFormat = this.book.cover - - // Update book cover path to new format - this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(7)).replace(/\\/g, '/') - this.book.cover = coverStripped.replace(this.path, `/s/book/${this.id}`) - Logger.debug(`[Audiobook] updated book cover to new format "${oldFormat}" => "${this.book.cover}"`) - } - hasUpdates = true - } - - // Check if book was removed from book dir - var bookCoverPath = this.book.cover ? this.book.cover.replace(/\\/g, '/') : null - if (bookCoverPath && bookCoverPath.startsWith('/s/book/')) { - // Fixing old cover paths - if (!this.book.coverFullPath) { - this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length)).replace(/\\/g, '/').replace(/\/\//g, '/') - Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`) - hasUpdates = true - } - - var coverStillExists = imageFiles.find(f => comparePaths(f.fullPath, this.book.coverFullPath)) - if (!coverStillExists) { - Logger.info(`[Audiobook] Local cover "${this.book.cover}" was removed | "${this.title}"`) - this.book.removeCover() - hasUpdates = true - } - } - - if (bookCoverPath && bookCoverPath.startsWith('/metadata')) { - // Fixing old cover paths - if (!this.book.coverFullPath) { - 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 - } - // metadata covers are stored in //books/:id/ - if (!await fs.pathExists(this.book.coverFullPath)) { - Logger.info(`[Audiobook] Cover in /metadata for "${this.title}" no longer exists - removing cover paths`) - this.book.removeCover() - hasUpdates = true - } - } - - if (this.book.cover && !this.book.coverFullPath) { - if (this.book.cover.startsWith('http')) { - Logger.debug(`[Audiobook] Still using http path for cover "${this.book.cover}" - should update to local`) - this.book.coverFullPath = this.book.cover - hasUpdates = true - } else { - Logger.warn(`[Audiobook] Full cover path still not set "${this.book.cover}"`) - } - } - - // If no cover set and image file exists then use it - if (!this.book.cover && imageFiles.length) { - var imagePathRelativeToBook = imageFiles[0].path.replace(this.path, '') - this.book.cover = Path.posix.join(`/s/book/${this.id}`, imagePathRelativeToBook) - this.book.coverFullPath = imageFiles[0].fullPath - Logger.info(`[Audiobook] Local cover was set to "${this.book.cover}" | "${this.title}"`) - hasUpdates = true - } - - return hasUpdates - } - - syncAudioFile(audioFile, fileScanData) { - var hasUpdates = audioFile.syncFile(fileScanData) - var track = this.tracks.find(t => t.ino === audioFile.ino) - if (track && track.syncFile(fileScanData)) { - hasUpdates = true - } - 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 - } - - isSearchMatch(search) { - var tagMatch = this.tags.filter(tag => { - return tag.toLowerCase().includes(search.toLowerCase().trim()) - }) - return this.book.isSearchMatch(search.toLowerCase().trim()) || tagMatch.length - } - - searchQuery(search) { - var matches = this.book.getQueryMatches(search.toLowerCase().trim()) - matches.tags = this.tags.filter(tag => { - return tag.toLowerCase().includes(search.toLowerCase().trim()) - }) - if (!matches.book && matches.tags.length) { - matches.book = 'tags' - matches.bookMatchText = matches.tags.join(', ') - } - return matches - } - - getAudioFileByIno(ino) { - return this.audioFiles.find(af => af.ino === ino) - } - - getAudioFileByPath(fullPath) { - return this.audioFiles.find(af => af.fullPath === fullPath) - } - - 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.filename ? Path.basename(file.filename, Path.extname(file.filename)) : `Chapter ${currChapterId}` - }) - currStartTime += file.duration - } - }) - } - } - - 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 coverAlreadyExists = await fs.pathExists(coverFilePath) - if (coverAlreadyExists) { - Logger.warn(`[Audiobook] Extract embedded cover art but cover already exists for "${this.title}" - bail`) - return false - } - - var success = await extractCoverArt(audioFileWithCover.fullPath, coverFilePath) - if (success) { - var coverRelPath = Path.join(coverDirRelPath, coverFilename).replace(/\\/g, '/').replace(/\/\//g, '/') - this.update({ book: { cover: coverRelPath, coverFullPath: audioFileWithCover.fullPath } }) - return coverRelPath - } - return false - } - - // 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)}..."`) - bookUpdatePayload.description = descriptionText - } - var readerText = await this.fetchTextFromTextFile('reader.txt') - if (readerText) { - Logger.debug(`[Audiobook] "${this.title}" found reader.txt updating narrator with "${readerText}"`) - 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) - if (xmlText) { - var opfMetadata = await parseOpfMetadataXML(xmlText) - // Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata) - if (opfMetadata) { - for (const key in opfMetadata) { - // Add genres only if genres are empty - if (key === 'genres') { - if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) { - bookUpdatePayload[key] = opfMetadata.genres - } - } else if (opfMetadata[key] && ((!this.book[key] && !bookUpdatePayload[key]) || opfMetadataOverrideDetails)) { - bookUpdatePayload[key] = opfMetadata[key] - } - } - } - } - } - - if (Object.keys(bookUpdatePayload).length) { - return this.update({ book: bookUpdatePayload }) - } - return false - } - - fetchTextFromTextFile(textfileName) { - var textFile = this.otherFiles.find(file => file.filename === textfileName) - if (!textFile) return false - return readTextFile(textFile.fullPath) - } - - // Audio file metadata tags map to book details (will not overwrite) - setDetailsFromFileMetadata(overrideExistingDetails = false) { - if (!this.audioFiles.length) return false - var audioFile = this.audioFiles[0] - return this.book.setDetailsFromFileMetadata(audioFile.metadata, overrideExistingDetails) - } - - // Returns null if file not found, true if file was updated, false if up to date - checkFileFound(fileFound, isAudioFile) { - var hasUpdated = false - - const arrayToCheck = isAudioFile ? this.audioFiles : this.otherFiles - - var existingFile = arrayToCheck.find(_af => _af.ino === fileFound.ino) - if (!existingFile) { - existingFile = arrayToCheck.find(_af => _af.path === fileFound.path) - if (existingFile) { - // file inode was updated - existingFile.ino = fileFound.ino - hasUpdated = true - } else { - // file not found - return null - } - } - - if (existingFile.path !== fileFound.path) { - existingFile.path = fileFound.path - existingFile.fullPath = fileFound.fullPath - hasUpdated = true - } else if (existingFile.fullPath !== fileFound.fullPath) { - existingFile.fullPath = fileFound.fullPath - 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 - } - - return hasUpdated - } - - checkScanData(dataFound, version) { - var hasUpdated = false - - if (this.isMissing) { - // Audiobook no longer missing - this.isMissing = false - hasUpdated = true - } - - if (dataFound.ino !== this.ino) { - this.ino = dataFound.ino - hasUpdated = true - } - - if (dataFound.folderId !== this.folderId) { - Logger.warn(`[Audiobook] Check scan audiobook changed folder ${this.folderId} -> ${dataFound.folderId}`) - this.folderId = dataFound.folderId - hasUpdated = true - } - - if (dataFound.path !== this.path) { - Logger.warn(`[Audiobook] Check scan audiobook changed path "${this.path}" -> "${dataFound.path}"`) - this.path = dataFound.path - this.fullPath = dataFound.fullPath - hasUpdated = true - } else if (dataFound.fullPath !== this.fullPath) { - Logger.warn(`[Audiobook] Check scan audiobook changed fullpath "${this.fullPath}" -> "${dataFound.fullPath}"`) - this.fullPath = dataFound.fullPath - 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 = [] - var existingOtherFileData = [] - - dataFound.audioFiles.forEach((af) => { - var audioFileFoundCheck = this.checkFileFound(af, true) - if (audioFileFoundCheck === null) { - newAudioFileData.push(af) - } else if (audioFileFoundCheck) { - hasUpdated = true - existingAudioFileData.push(af) - } else { - existingAudioFileData.push(af) - } - }) - - dataFound.otherFiles.forEach((otherFileData) => { - var fileFoundCheck = this.checkFileFound(otherFileData, false) - if (fileFoundCheck === null) { - newOtherFileData.push(otherFileData) - } else if (fileFoundCheck) { - hasUpdated = true - existingOtherFileData.push(otherFileData) - } else { - existingOtherFileData.push(otherFileData) - } - }) - - const audioFilesRemoved = [] - const otherFilesRemoved = [] - - // Remove audio files not found (inodes will all be up to date at this point) - this.audioFiles = this.audioFiles.filter(af => { - if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) { - audioFilesRemoved.push(af.toJSON()) - return false - } - return true - }) - - // Remove all tracks that were associated with removed audio files - if (audioFilesRemoved.length) { - const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino) - this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino)) - this.checkUpdateMissingTracks() - hasUpdated = true - } - - // Remove other files not found - this.otherFiles = this.otherFiles.filter(otherFile => { - if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) { - otherFilesRemoved.push(otherFile.toJSON()) - - // Check remove cover - if (otherFile.fullPath === this.book.coverFullPath) { - Logger.debug(`[Audiobook] "${this.title}" Check scan book cover removed`) - this.book.removeCover() - } - - return false - } - return true - }) - - if (otherFilesRemoved.length) { - hasUpdated = true - } - - // Check if invalid (has no audio files or ebooks) - if (!this.audioFilesToInclude.length && !this.ebooks.length && !newAudioFileData.length && !newOtherFileData.length) { - this.isInvalid = true - } - - if (hasUpdated) { - this.setLastScan(version) - } - - return { - updated: hasUpdated, - newAudioFileData, - newOtherFileData, - audioFilesRemoved, - otherFilesRemoved, - existingAudioFileData, // Existing file data may get re-scanned if forceRescan is set - existingOtherFileData - } - } - - // Temp fix for cover is set but coverFullPath is not set - 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(global.MetadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/') - } - if (newFullCoverPath) { - Logger.debug(`[Audiobook] "${this.title}" fixing full cover path "${this.book.cover}" => "${newFullCoverPath}"`) - this.update({ book: { fullCoverPath: newFullCoverPath } }) - return true - } - 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 \ No newline at end of file diff --git a/server/objects/legacy/AudiobookFile.js b/server/objects/legacy/AudiobookFile.js deleted file mode 100644 index b57be8f4..00000000 --- a/server/objects/legacy/AudiobookFile.js +++ /dev/null @@ -1,69 +0,0 @@ -class AudiobookFile { - constructor(data) { - this.ino = null - this.filetype = null - this.filename = null - 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) { - this.construct(data) - } - } - - get isOPFFile() { - return this.ext ? this.ext.toLowerCase() === '.opf' : false - } - - toJSON() { - return { - ino: this.ino || null, - filetype: this.filetype, - filename: this.filename, - ext: this.ext, - path: this.path, - fullPath: this.fullPath, - size: this.size, - mtimeMs: this.mtimeMs, - ctimeMs: this.ctimeMs, - birthtimeMs: this.birthtimeMs, - addedAt: this.addedAt - } - } - - construct(data) { - this.ino = data.ino || null - this.filetype = data.filetype - this.filename = data.filename - 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 - } - - setData(data) { - this.ino = data.ino || null - this.filetype = data.filetype - this.filename = data.filename - 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() - } -} -module.exports = AudiobookFile \ No newline at end of file diff --git a/server/objects/legacy/Author.js b/server/objects/legacy/Author.js deleted file mode 100644 index 04d70e4e..00000000 --- a/server/objects/legacy/Author.js +++ /dev/null @@ -1,72 +0,0 @@ -const { getId } = require('../../utils/index') -const Logger = require('../../Logger') - -class Author { - constructor(author = null) { - this.id = null - this.name = null - this.description = null - this.asin = null - this.image = null - this.imageFullPath = null - - this.createdAt = null - this.lastUpdate = null - - if (author) { - this.construct(author) - } - } - - construct(author) { - this.id = author.id - this.name = author.name - this.description = author.description - this.asin = author.asin - this.image = author.image - this.imageFullPath = author.imageFullPath - - this.createdAt = author.createdAt - this.lastUpdate = author.lastUpdate - } - - toJSON() { - return { - id: this.id, - name: this.name, - description: this.description, - asin: this.asin, - image: this.image, - imageFullPath: this.imageFullPath, - createdAt: this.createdAt, - lastUpdate: this.lastUpdate - } - } - - setData(data) { - this.id = data.id ? data.id : getId('per') - this.name = data.name - this.description = data.description - this.asin = data.asin || null - this.image = data.image || null - this.imageFullPath = data.imageFullPath || null - this.createdAt = Date.now() - this.lastUpdate = Date.now() - } - - update(payload) { - var hasUpdates = false - for (const key in payload) { - if (this[key] === undefined) continue; - if (this[key] !== payload[key]) { - hasUpdates = true - this[key] = payload[key] - } - } - if (hasUpdates) { - this.lastUpdate = Date.now() - } - return hasUpdates - } -} -module.exports = Author \ No newline at end of file diff --git a/server/objects/legacy/Book.js b/server/objects/legacy/Book.js deleted file mode 100644 index 9281dd27..00000000 --- a/server/objects/legacy/Book.js +++ /dev/null @@ -1,417 +0,0 @@ -const Path = require('path') -const Logger = require('../../Logger') -const parseAuthors = require('../../utils/parseNameString') - -class Book { - constructor(book = null) { - this.title = null - this.subtitle = null - this.author = null - this.authorFL = null - this.authorLF = null - this.authors = [] - this.narrator = null - this.narratorFL = null - this.series = null - this.volumeNumber = null - this.publishYear = null - this.publisher = null - this.description = null - this.isbn = null - this.asin = null - this.language = null - this.cover = null - this.coverFullPath = null - this.genres = [] - - this.lastUpdate = null - - // Should not continue looking up a cover when it is not findable - this.lastCoverSearch = null - this.lastCoverSearchTitle = null - this.lastCoverSearchAuthor = null - - if (book) { - this.construct(book) - } - } - - get _title() { return this.title || '' } - get _subtitle() { return this.subtitle || '' } - get _narrator() { return this.narrator || '' } - get _author() { return this.authorFL || '' } - get _series() { return this.series || '' } - get _authorsList() { return this._author.split(', ') } - get _narratorsList() { return this._narrator.split(', ') } - get _genres() { return this.genres || [] } - get _language() { return this.language || '' } - get _isbn() { return this.isbn || '' } - 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 - var timeSinceLastSearch = Date.now() - this.lastCoverSearch - return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup - } - - construct(book) { - this.title = book.title - this.subtitle = book.subtitle || null - this.author = book.author - this.authors = (book.authors || []).map(a => ({ ...a })) - this.authorFL = book.authorFL || null - this.authorLF = book.authorLF || null - this.narrator = book.narrator || book.narrarator || null // Mispelled initially... need to catch those - this.narratorFL = book.narratorFL || null - this.series = book.series - this.volumeNumber = book.volumeNumber || null - this.publishYear = book.publishYear - this.publisher = book.publisher - this.description = book.description - this.isbn = book.isbn || null - this.asin = book.asin || null - this.language = book.language || null - this.cover = book.cover - this.coverFullPath = book.coverFullPath || null - this.genres = book.genres - this.lastUpdate = book.lastUpdate || Date.now() - this.lastCoverSearch = book.lastCoverSearch || null - this.lastCoverSearchTitle = book.lastCoverSearchTitle || null - this.lastCoverSearchAuthor = book.lastCoverSearchAuthor || null - - // narratorFL added in v1.6.21 to support multi-narrators - if (this.narrator && !this.narratorFL) { - this.setParseNarrator(this.narrator) - } - } - - toJSON() { - return { - title: this.title, - subtitle: this.subtitle, - author: this.author, - authors: this.authors, - authorFL: this.authorFL, - authorLF: this.authorLF, - narrator: this.narrator, - narratorFL: this.narratorFL, - series: this.series, - volumeNumber: this.volumeNumber, - publishYear: this.publishYear, - publisher: this.publisher, - description: this.description, - isbn: this.isbn, - asin: this.asin, - language: this.language, - cover: this.cover, - coverFullPath: this.coverFullPath, - genres: this.genres, - lastUpdate: this.lastUpdate, - lastCoverSearch: this.lastCoverSearch, - lastCoverSearchTitle: this.lastCoverSearchTitle, - lastCoverSearchAuthor: this.lastCoverSearchAuthor - } - } - - setParseAuthor(author) { - if (!author) { - var hasUpdated = this.authorFL || this.authorLF - this.authorFL = null - this.authorLF = null - return hasUpdated - } - try { - var { authorLF, authorFL } = parseAuthors.parse(author) - var hasUpdated = authorLF !== this.authorLF || authorFL !== this.authorFL - this.authorFL = authorFL || null - this.authorLF = authorLF || null - return hasUpdated - } catch (err) { - Logger.error('[Book] Parse authors failed', err) - return false - } - } - - setParseNarrator(narrator) { - if (!narrator) { - var hasUpdated = this.narratorFL - this.narratorFL = null - return hasUpdated - } - try { - var { authorFL } = parseAuthors.parse(narrator) - var hasUpdated = authorFL !== this.narratorFL - this.narratorFL = authorFL || null - return hasUpdated - } catch (err) { - Logger.error('[Book] Parse narrator failed', err) - return false - } - } - - setData(data) { - this.title = data.title || null - this.subtitle = data.subtitle || null - this.author = data.author || null - this.authors = data.authors || [] - this.narrator = data.narrator || data.narrarator || null - this.series = data.series || null - this.volumeNumber = data.volumeNumber || null - this.publishYear = data.publishYear || null - this.description = data.description || null - this.isbn = data.isbn || null - this.asin = data.asin || null - this.language = data.language || null - this.cover = data.cover || null - this.coverFullPath = data.coverFullPath || null - this.genres = data.genres || [] - this.lastUpdate = Date.now() - this.lastCoverSearch = data.lastCoverSearch || null - this.lastCoverSearchTitle = data.lastCoverSearchTitle || null - this.lastCoverSearchAuthor = data.lastCoverSearchAuthor || null - - if (data.author) { - this.setParseAuthor(this.author) - } - if (data.narrator) { - this.setParseNarrator(this.narrator) - } - } - - update(payload) { - var hasUpdates = false - - // Clean cover paths if passed - if (payload.cover) { - if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) { - payload.cover = payload.cover.replace(/\\/g, '/') - if (payload.coverFullPath) payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/') - else { - Logger.warn(`[Book] "${this.title}" updating book cover to "${payload.cover}" but no full path was passed`) - } - } - } else if (payload.coverFullPath) { - Logger.warn(`[Book] "${this.title}" updating book full cover path to "${payload.coverFullPath}" but no relative path was passed`) - payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/') - } - - for (const key in payload) { - if (payload[key] === undefined) continue; - - if (key === 'genres') { - if (payload['genres'] === null && this.genres !== null) { - this.genres = [] - hasUpdates = true - } else if (payload['genres'].join(',') !== this.genres.join(',')) { - this.genres = payload['genres'] - hasUpdates = true - } - } else if (key === 'author') { - if (this.author !== payload.author) { - this.author = payload.author || null - hasUpdates = true - } - if (this.setParseAuthor(this.author)) { - hasUpdates = true - } - } else if (key === 'narrator') { - if (this.narrator !== payload.narrator) { - this.narrator = payload.narrator || null - hasUpdates = true - } - if (this.setParseNarrator(this.narrator)) { - hasUpdates = true - } - } else if (this[key] !== undefined && payload[key] !== this[key]) { - this[key] = payload[key] - hasUpdates = true - } - } - - if (hasUpdates) { - this.lastUpdate = Date.now() - } - - return hasUpdates - } - - updateLastCoverSearch(coverWasFound) { - this.lastCoverSearch = coverWasFound ? null : Date.now() - this.lastCoverSearchAuthor = coverWasFound ? null : this.authorFL - this.lastCoverSearchTitle = coverWasFound ? null : this.title - } - - updateCover(cover, coverFullPath) { - if (!cover) return false - if (!cover.startsWith('http:') && !cover.startsWith('https:')) { - cover = cover.replace(/\\/g, '/') - this.coverFullPath = coverFullPath.replace(/\\/g, '/') - } else { - this.coverFullPath = cover - } - this.cover = cover - this.lastUpdate = Date.now() - return true - } - - removeCover() { - this.cover = null - this.coverFullPath = null - this.lastUpdate = Date.now() - } - - // If audiobook directory path was changed, check and update properties set from dirnames - // May be worthwhile checking if these were manually updated and not override manual updates - syncPathsUpdated(audiobookData) { - var keysToSync = ['author', 'title', 'series', 'publishYear', 'volumeNumber'] - var syncPayload = {} - keysToSync.forEach((key) => { - if (audiobookData[key]) syncPayload[key] = audiobookData[key] - }) - if (!Object.keys(syncPayload).length) return false - return this.update(syncPayload) - } - - isSearchMatch(search) { - return this._title.toLowerCase().includes(search) || this._subtitle.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search) - } - - getQueryMatches(search) { - var titleMatch = this._title.toLowerCase().includes(search) - var subtitleMatch = this._subtitle.toLowerCase().includes(search) - - var authorsMatched = this._authorsList.filter(auth => auth.toLowerCase().includes(search)) - - // var authorMatch = this._author.toLowerCase().includes(search) - var seriesMatch = this._series.toLowerCase().includes(search) - - // ISBN match has to be exact to prevent isbn matches to flood results. Remove dashes since isbn might have those - var isbnMatch = this._isbn.toLowerCase().replace(/-/g, '') === search.replace(/-/g, '') - - var asinMatch = this._asin.toLowerCase() === search - - var bookMatchKey = titleMatch ? 'title' : subtitleMatch ? 'subtitle' : authorsMatched.length ? 'authorFL' : seriesMatch ? 'series' : isbnMatch ? 'isbn' : asinMatch ? 'asin' : false - - var bookMatchText = bookMatchKey ? this[bookMatchKey] : '' - return { - book: bookMatchKey, - bookMatchText, - authors: authorsMatched.length ? authorsMatched : false, - series: seriesMatch ? this._series : false - } - } - - parseGenresTag(genreTag) { - if (!genreTag || !genreTag.length) return [] - var separators = ['/', '//', ';'] - for (let i = 0; i < separators.length; i++) { - if (genreTag.includes(separators[i])) { - return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g) - } - } - return [genreTag] - } - - setDetailsFromFileMetadata(audioFileMetadata, overrideExistingDetails = false) { - const MetadataMapArray = [ - { - tag: 'tagComposer', - key: 'narrator' - }, - { - tag: 'tagDescription', - key: 'description' - }, - { - tag: 'tagPublisher', - key: 'publisher' - }, - { - tag: 'tagDate', - key: 'publishYear' - }, - { - tag: 'tagSubtitle', - key: 'subtitle' - }, - { - tag: 'tagAlbum', - altTag: 'tagTitle', - key: 'title', - }, - { - tag: 'tagArtist', - altTag: 'tagAlbumArtist', - key: 'author' - }, - { - tag: 'tagGenre', - key: 'genres' - }, - { - tag: 'tagSeries', - key: 'series' - }, - { - tag: 'tagSeriesPart', - key: 'volumeNumber' - }, - { - tag: 'tagIsbn', - key: 'isbn' - }, - { - tag: 'tagLanguage', - key: 'language' - }, - { - tag: 'tagASIN', - key: 'asin' - } - ] - - var updatePayload = {} - - // Metadata is only mapped to the book if it is empty - MetadataMapArray.forEach((mapping) => { - var value = audioFileMetadata[mapping.tag] - var tagToUse = mapping.tag - if (!value && mapping.altTag) { - value = audioFileMetadata[mapping.altTag] - tagToUse = mapping.altTag - } - if (value) { - // Genres can contain multiple - if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key] || overrideExistingDetails)) { - updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[tagToUse]) - // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`) - } else if (!this[mapping.key] || overrideExistingDetails) { - updatePayload[mapping.key] = audioFileMetadata[tagToUse] - // Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`) - } - } - }) - - if (Object.keys(updatePayload).length) { - return this.update(updatePayload) - } - return false - } -} -module.exports = Book \ No newline at end of file diff --git a/server/objects/legacy/StreamManager.js b/server/objects/legacy/StreamManager.js deleted file mode 100644 index 89dfc0e3..00000000 --- a/server/objects/legacy/StreamManager.js +++ /dev/null @@ -1,231 +0,0 @@ -const Stream = require('../Stream') -// const StreamTest = require('./test/StreamTest') -const Logger = require('../../Logger') -const fs = require('fs-extra') -const Path = require('path') - -class StreamManager { - constructor(db, emitter, clientEmitter) { - this.db = db - - this.emitter = emitter - this.clientEmitter = clientEmitter - - this.streams = [] - this.StreamsPath = Path.join(global.MetadataPath, 'streams') - } - - getStream(streamId) { - return this.streams.find(s => s.id === streamId) - } - - removeStream(stream) { - this.streams = this.streams.filter(s => s.id !== stream.id) - } - - async openStream(client, libraryItem, transcodeOptions = {}) { - if (!client || !client.user) { - Logger.error('[StreamManager] Cannot open stream invalid client', client) - return - } - var stream = new Stream(this.StreamsPath, client, libraryItem, transcodeOptions) - - stream.on('closed', () => { - this.removeStream(stream) - }) - - this.streams.push(stream) - - await stream.generatePlaylist() - stream.start() - - Logger.info('Stream Opened for client', client.user.username, 'for item', stream.itemTitle, 'with streamId', stream.id) - - client.stream = stream - client.user.stream = stream.id - - return stream - } - - ensureStreamsDir() { - return fs.ensureDir(this.StreamsPath) - } - - removeOrphanStreamFiles(streamId) { - try { - var StreamsPath = Path.join(this.StreamsPath, streamId) - return fs.remove(StreamsPath) - } catch (error) { - Logger.debug('No orphan stream', streamId) - return false - } - } - - async removeOrphanStreams() { - try { - var dirs = await fs.readdir(this.StreamsPath) - if (!dirs || !dirs.length) return true - - await Promise.all(dirs.map(async (dirname) => { - var fullPath = Path.join(this.StreamsPath, dirname) - Logger.info(`Removing Orphan Stream ${dirname}`) - return fs.remove(fullPath) - })) - return true - } catch (error) { - Logger.debug('No orphan stream', error) - return false - } - } - - async openStreamApiRequest(res, user, libraryItem) { - Logger.info(`[StreamManager] User "${user.username}" open stream request for "${libraryItem.media.metadata.title}"`) - var client = { - user - } - var stream = await this.openStream(client, libraryItem) - this.db.updateUserStream(client.user.id, stream.id) - - res.json({ - libraryItemId: libraryItem.id, - startTime: stream.startTime, - streamId: stream.id, - streamUrl: stream.clientPlaylistUri - }) - } - - async openStreamSocketRequest(socket, libraryItemId) { - Logger.info('[StreamManager] Open Stream Request', socket.id, libraryItemId) - var libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId) - var client = socket.sheepClient - - if (client.stream) { - Logger.info('Closing client stream first', client.stream.id) - await client.stream.close() - client.user.stream = null - client.stream = null - } - - var stream = await this.openStream(client, libraryItem) - this.db.updateUserStream(client.user.id, stream.id) - } - - async closeStreamRequest(socket) { - Logger.info('Close Stream Request', socket.id) - var client = socket.sheepClient - if (!client || !client.stream) { - Logger.error('No stream for client', (client && client.user) ? client.user.username : 'No Client') - client.socket.emit('stream_closed', 'n/a') - return - } - // var streamId = client.stream.id - await client.stream.close() - client.user.stream = null - client.stream = null - this.db.updateUserStream(client.user.id, null) - } - - async closeStreamApiRequest(userId, streamId) { - Logger.info('[StreamManager] Close Stream Api Request', streamId) - - var stream = this.streams.find(s => s.id === streamId) - if (!stream) { - Logger.warn('[StreamManager] Stream not found', streamId) - return - } - - if (!stream.client || !stream.client.user || stream.client.user.id !== userId) { - Logger.warn(`[StreamManager] Stream close request from invalid user ${userId}`, stream.client) - return - } - - stream.client.user.stream = null - stream.client.stream = null - this.db.updateUserStream(stream.client.user.id, null) - - await stream.close() - - this.streams = this.streams.filter(s => s.id !== streamId) - Logger.info(`[StreamManager] Stream ${streamId} closed via API request by ${userId}`) - } - - streamSync(socket, syncData) { - const client = socket.sheepClient - if (!client || !client.stream) { - Logger.error('[StreamManager] streamSync: No stream for client', (client && client.user) ? client.user.id : 'No Client') - return - } - if (client.stream.id !== syncData.streamId) { - Logger.error('[StreamManager] streamSync: Stream id mismatch on stream update', syncData.streamId, client.stream.id) - return - } - if (!client.user) { - Logger.error('[StreamManager] streamSync: No User for client', client) - return - } - // const { timeListened, currentTime, streamId } = syncData - var listeningSession = client.stream.syncStream(syncData) - - if (listeningSession && listeningSession.timeListening > 0) { - // Save listening session - var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id) - if (existingListeningSession) { - this.db.updateEntity('session', listeningSession) - } else { - this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session - this.db.insertEntity('session', listeningSession) - } - } - - var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream) - this.db.updateEntity('user', client.user) - - if (userAudiobook) { - this.clientEmitter(client.user.id, 'current_user_audiobook_update', { - id: userAudiobook.audiobookId, - data: userAudiobook.toJSON() - }) - } - } - - streamSyncFromApi(req, res) { - var user = req.user - var syncData = req.body - - var stream = this.streams.find(s => s.id === syncData.streamId) - if (!stream) { - Logger.error(`[StreamManager] streamSyncFromApi stream not found ${syncData.streamId}`) - return res.status(404).send('Stream not found') - } - if (stream.userToken !== user.token) { - Logger.error(`[StreamManager] streamSyncFromApi Invalid stream not owned by user`) - return res.status(500).send('Invalid stream auth') - } - - var listeningSession = stream.syncStream(syncData) - - if (listeningSession && listeningSession.timeListening > 0) { - // Save listening session - var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id) - if (existingListeningSession) { - this.db.updateEntity('session', listeningSession) - } else { - this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session - this.db.insertEntity('session', listeningSession) - } - } - - var userAudiobook = user.updateAudiobookProgressFromStream(stream) - this.db.updateEntity('user', user) - - if (userAudiobook) { - this.clientEmitter(user.id, 'current_user_audiobook_update', { - id: userAudiobook.audiobookId, - data: userAudiobook.toJSON() - }) - } - - res.sendStatus(200) - } -} -module.exports = StreamManager \ No newline at end of file diff --git a/server/objects/legacy/UserAudiobookData.js b/server/objects/legacy/UserAudiobookData.js deleted file mode 100644 index 41b88e91..00000000 --- a/server/objects/legacy/UserAudiobookData.js +++ /dev/null @@ -1,136 +0,0 @@ -const Logger = require('../../Logger') -const AudioBookmark = require('../user/AudioBookmark') - -class UserAudiobookData { - constructor(progress) { - this.audiobookId = null - - this.totalDuration = null // seconds - this.progress = null // 0 to 1 - this.currentTime = null // seconds - this.isRead = false - this.lastUpdate = null - this.startedAt = null - this.finishedAt = null - this.bookmarks = [] - - if (progress) { - this.construct(progress) - } - } - - bookmarksToJSON() { - if (!this.bookmarks) return [] - return this.bookmarks.filter((b) => { - if (!b.toJSON) { - Logger.error(`[UserAudiobookData] Invalid bookmark ${JSON.stringify(b)}`) - return false - } - return true - }).map(b => b.toJSON()) - } - - toJSON() { - return { - audiobookId: this.audiobookId, - totalDuration: this.totalDuration, - progress: this.progress, - currentTime: this.currentTime, - isRead: this.isRead, - lastUpdate: this.lastUpdate, - startedAt: this.startedAt, - finishedAt: this.finishedAt, - bookmarks: this.bookmarksToJSON() - } - } - - construct(progress) { - this.audiobookId = progress.audiobookId - this.totalDuration = progress.totalDuration - this.progress = progress.progress - this.currentTime = progress.currentTime - this.isRead = !!progress.isRead - this.lastUpdate = progress.lastUpdate - this.startedAt = progress.startedAt - this.finishedAt = progress.finishedAt || null - if (progress.bookmarks) { - this.bookmarks = progress.bookmarks.map(b => new AudioBookmark(b)) - } else { - this.bookmarks = [] - } - } - - updateProgressFromStream(stream) { - this.audiobookId = stream.libraryItemId - this.totalDuration = stream.totalDuration - this.progress = stream.clientProgress - this.currentTime = stream.clientCurrentTime - this.lastUpdate = Date.now() - - if (!this.startedAt) { - this.startedAt = Date.now() - } - - // If has < 10 seconds remaining mark as read - var timeRemaining = this.totalDuration - this.currentTime - if (timeRemaining < 10) { - this.isRead = true - this.progress = 1 - this.finishedAt = Date.now() - } else { - this.isRead = false - this.finishedAt = null - } - } - - update(payload) { - var hasUpdates = false - for (const key in payload) { - if (this[key] !== undefined && payload[key] !== this[key]) { - if (key === 'isRead') { - if (!payload[key]) { // Updating to Not Read - Reset progress and current time - this.finishedAt = null - this.progress = 0 - this.currentTime = 0 - } else { // Updating to Read - if (!this.finishedAt) this.finishedAt = Date.now() - this.progress = 1 - } - } - - this[key] = payload[key] - hasUpdates = true - } - } - if (!this.startedAt) { - this.startedAt = Date.now() - } - if (hasUpdates) { - this.lastUpdate = Date.now() - } - return hasUpdates - } - - checkBookmarkExists(time) { - return this.bookmarks.find(bm => bm.time === time) - } - - createBookmark(time, title) { - var newBookmark = new AudioBookmark() - newBookmark.setData(time, title) - this.bookmarks.push(newBookmark) - return newBookmark - } - - updateBookmark(time, title) { - var bookmark = this.bookmarks.find(bm => bm.time === time) - if (!bookmark) return false - bookmark.title = title - return bookmark - } - - deleteBookmark(time) { - this.bookmarks = this.bookmarks.filter(bm => bm.time !== time) - } -} -module.exports = UserAudiobookData \ No newline at end of file diff --git a/server/objects/legacy/UserListeningSession.js b/server/objects/legacy/UserListeningSession.js deleted file mode 100644 index 98edaa65..00000000 --- a/server/objects/legacy/UserListeningSession.js +++ /dev/null @@ -1,98 +0,0 @@ -const Logger = require('../../Logger') -const date = require('date-and-time') -const { getId } = require('../../utils/index') - -class UserListeningSession { - constructor(session) { - this.id = null - this.sessionType = 'listeningSession' - this.userId = null - this.audiobookId = null - this.audiobookTitle = null - this.audiobookAuthor = null - this.audiobookDuration = 0 - this.audiobookGenres = [] - - this.date = null - this.dayOfWeek = null - - this.timeListening = null - this.lastUpdate = null - this.startedAt = null - - if (session) { - this.construct(session) - } - } - - toJSON() { - return { - id: this.id, - sessionType: this.sessionType, - userId: this.userId, - audiobookId: this.audiobookId, - audiobookTitle: this.audiobookTitle, - audiobookAuthor: this.audiobookAuthor, - audiobookDuration: this.audiobookDuration, - audiobookGenres: [...this.audiobookGenres], - date: this.date, - dayOfWeek: this.dayOfWeek, - timeListening: this.timeListening, - lastUpdate: this.lastUpdate, - startedAt: this.startedAt - } - } - - construct(session) { - this.id = session.id - this.sessionType = session.sessionType - this.userId = session.userId - this.audiobookId = session.audiobookId - this.audiobookTitle = session.audiobookTitle - this.audiobookAuthor = session.audiobookAuthor - this.audiobookDuration = session.audiobookDuration || 0 - this.audiobookGenres = session.audiobookGenres - - this.date = session.date - this.dayOfWeek = session.dayOfWeek - - this.timeListening = session.timeListening || null - this.lastUpdate = session.lastUpdate || null - this.startedAt = session.startedAt - } - - setData(libraryItem, user) { - this.id = getId('ls') - this.userId = user.id - this.audiobookId = libraryItem.id - // TODO: For podcasts this needs to be generic - this.audiobookTitle = libraryItem.media.metadata.title || '' - this.audiobookAuthor = libraryItem.media.metadata.authorName || '' - this.audiobookDuration = libraryItem.media.duration || 0 - this.audiobookGenres = [...libraryItem.media.metadata.genres] - - this.timeListening = 0 - this.lastUpdate = Date.now() - this.startedAt = Date.now() - } - - addListeningTime(timeListened) { - if (timeListened && !isNaN(timeListened)) { - if (!this.date) { - // Set date info on first listening update - this.date = date.format(new Date(), 'YYYY-MM-DD') - this.dayOfWeek = date.format(new Date(), 'dddd') - } - - this.timeListening += timeListened - this.lastUpdate = Date.now() - } - } - - // New date since start of listening session - checkDateRollover() { - if (!this.date) return false - return date.format(new Date(), 'YYYY-MM-DD') !== this.date - } -} -module.exports = UserListeningSession \ No newline at end of file diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js index 705f5517..bcb77722 100644 --- a/server/utils/dbMigration.js +++ b/server/utils/dbMigration.js @@ -7,9 +7,6 @@ const { PlayMethod } = require('./constants') const { getId } = require('./index') const Logger = require('../Logger') -const LegacyAudiobook = require('../objects/legacy/Audiobook') -const UserAudiobookData = require('../objects/legacy/UserAudiobookData') - const Library = require('../objects/Library') const LibraryItem = require('../objects/LibraryItem') const Book = require('../objects/mediaTypes/Book') @@ -40,14 +37,17 @@ var existingDbSeries = [] async function loadAudiobooks() { var audiobookPath = Path.join(global.ConfigPath, 'audiobooks') + Logger.debug(`[dbMigration] loadAudiobooks path ${audiobookPath}`) var pathExists = await fs.pathExists(audiobookPath) if (!pathExists) { + Logger.debug(`[dbMigration] loadAudiobooks path does not exist ${audiobookPath}`) return [] } var audiobooksDb = new njodb.Database(audiobookPath) return audiobooksDb.select(() => true).then((results) => { - return results.data.map(a => new LegacyAudiobook(a)) + Logger.debug(`[dbMigration] loadAudiobooks select results ${results.data.length}`) + return results.data }) } @@ -65,7 +65,7 @@ function makeAuthorsFromOldAb(authorsList) { var newAuthor = new Author() newAuthor.setData({ name: authorName }) authorsToAdd.push(newAuthor) - Logger.debug(`>>> Created new author named "${authorName}"`) + // Logger.debug(`>>> Created new author named "${authorName}"`) return newAuthor.toJSONMinimal() }) } @@ -96,7 +96,8 @@ function makeFilesFromOldAb(audiobook) { var libraryFiles = [] var ebookFiles = [] - var audioFiles = audiobook._audioFiles.map((af) => { + var _audioFiles = audiobook.audioFiles || [] + var audioFiles = _audioFiles.map((af) => { var fileMetadata = new FileMetadata(af) fileMetadata.path = af.fullPath fileMetadata.relPath = getRelativePath(af.fullPath, audiobook.fullPath) @@ -118,7 +119,8 @@ function makeFilesFromOldAb(audiobook) { return newAudioFile }) - audiobook._otherFiles.forEach((file) => { + var _otherFiles = audiobook.otherFiles || [] + _otherFiles.forEach((file) => { var fileMetadata = new FileMetadata(file) fileMetadata.path = file.fullPath fileMetadata.relPath = getRelativePath(file.fullPath, audiobook.fullPath) @@ -182,10 +184,10 @@ function makeLibraryItemFromOldAb(audiobook) { var bookMetadata = new BookMetadata(audiobook.book) bookMetadata.publishedYear = audiobook.book.publishYear || null if (audiobook.book.narrator) { - bookMetadata.narrators = audiobook.book._narratorsList + bookMetadata.narrators = (audiobook.book.narrator || '').split(', ') } // Returns array of json minimal authors - bookMetadata.authors = makeAuthorsFromOldAb(audiobook.book._authorsList) + bookMetadata.authors = makeAuthorsFromOldAb((audiobook.book.authorFL || '').split(', ')) // Returns array of json minimal series if (audiobook.book.series) { @@ -276,7 +278,7 @@ function cleanUserObject(db, userObj) { cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks) } - var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object + var userAudiobookData = userObj.audiobooks[audiobookId] var liProgress = new MediaProgress() // New Progress Object liProgress.id = userAudiobookData.audiobookId liProgress.libraryItemId = userAudiobookData.audiobookId