From 68b13ae45f2d96f76943d864f0535f37f5817cb1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 15 Mar 2022 18:57:15 -0500 Subject: [PATCH] New data model migration for users, bookmarks and playback sessions --- server/Db.js | 42 +--- server/PlaybackSessionManager.js | 8 + server/Server.js | 16 +- server/controllers/UserController.js | 2 +- server/objects/Stream.js | 2 +- .../objects/{ => legacy}/UserAudiobookData.js | 4 +- .../{ => legacy}/UserListeningSession.js | 4 +- server/objects/metadata/BookMetadata.js | 6 +- server/objects/metadata/PodcastMetadata.js | 4 + server/objects/{ => user}/AudioBookmark.js | 6 +- server/objects/user/LibraryItemProgress.js | 99 +++++++++ server/objects/user/PlaybackSession.js | 103 +++++++++ server/objects/{ => user}/User.js | 196 +++++++++--------- server/scanner/Scanner.js | 25 --- server/utils/constants.js | 6 + server/utils/dbMigration.js | 123 ++++++++++- server/utils/libraryHelpers.js | 8 +- 17 files changed, 462 insertions(+), 192 deletions(-) create mode 100644 server/PlaybackSessionManager.js rename server/objects/{ => legacy}/UserAudiobookData.js (97%) rename server/objects/{ => legacy}/UserListeningSession.js (96%) rename server/objects/{ => user}/AudioBookmark.js (73%) create mode 100644 server/objects/user/LibraryItemProgress.js create mode 100644 server/objects/user/PlaybackSession.js rename server/objects/{ => user}/User.js (65%) diff --git a/server/Db.js b/server/Db.js index 311829cf..a3fdfe40 100644 --- a/server/Db.js +++ b/server/Db.js @@ -6,7 +6,7 @@ const Logger = require('./Logger') const { version } = require('../package.json') // const Audiobook = require('./objects/Audiobook') const LibraryItem = require('./objects/LibraryItem') -const User = require('./objects/User') +const User = require('./objects/user/User') const UserCollection = require('./objects/UserCollection') const Library = require('./objects/Library') const Author = require('./objects/entities/Author') @@ -235,46 +235,6 @@ class Db { }) } - async updateAudiobook(audiobook) { - if (audiobook && audiobook.saveAbMetadata) { - // TODO: Book may have updates where this save is not necessary - // add check first if metadata update is needed - await audiobook.saveAbMetadata() - } else { - Logger.error(`[Db] Invalid audiobook object passed to updateAudiobook`, audiobook) - } - - return this.libraryItemsDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => { - Logger.debug(`[DB] Audiobook updated ${results.updated}`) - return true - }).catch((error) => { - Logger.error(`[DB] Audiobook update failed ${error}`) - return false - }) - } - - insertAudiobook(audiobook) { - return this.insertAudiobooks([audiobook]) - } - - async insertAudiobooks(audiobooks) { - // TODO: Books may have updates where this save is not necessary - // add check first if metadata update is needed - await Promise.all(audiobooks.map(async (ab) => { - if (ab && ab.saveAbMetadata) return ab.saveAbMetadata() - return null - })) - - return this.libraryItemsDb.insert(audiobooks).then((results) => { - Logger.debug(`[DB] Audiobooks inserted ${results.inserted}`) - this.audiobooks = this.audiobooks.concat(audiobooks) - return true - }).catch((error) => { - Logger.error(`[DB] Audiobooks insert failed ${error}`) - return false - }) - } - updateUserStream(userId, streamId) { return this.usersDb.update((record) => record.id === userId, (user) => { user.stream = streamId diff --git a/server/PlaybackSessionManager.js b/server/PlaybackSessionManager.js new file mode 100644 index 00000000..4f65db5b --- /dev/null +++ b/server/PlaybackSessionManager.js @@ -0,0 +1,8 @@ + + +class PlaybackSessionManager { + constructor() { + + } +} +module.exports = PlaybackSessionManager \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index 2344f444..bd8403d1 100644 --- a/server/Server.js +++ b/server/Server.js @@ -108,12 +108,13 @@ class Server { await this.streamManager.removeOrphanStreams() await this.downloadManager.removeOrphanDownloads() - - await this.db.init() - - if (version.localeCompare('1.7.3') < 0) { - await dbMigration(this.db) + if (version.localeCompare('1.7.3') < 0) { // Old version data model migration + await dbMigration.migrateUserData(this.db) // Db not yet loaded + await this.db.init() + await dbMigration.migrateLibraryItems(this.db) // TODO: Eventually remove audiobooks db when stable + } else { + await this.db.init() } this.auth.init() @@ -125,11 +126,6 @@ class Server { await this.backupManager.init() await this.logManager.init() - // Only fix duplicate ids once on upgrade - if (this.db.previousVersion === '1.0.0') { - Logger.info(`[Server] Running scan for duplicate book IDs`) - await this.scanner.fixDuplicateIds() - } // If server upgrade and last version was 1.7.0 or earlier - add abmetadata files // if (this.db.checkPreviousVersionIsBefore('1.7.1')) { // TODO: wait until stable diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index ab0badad..b8a82e89 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,5 +1,5 @@ const Logger = require('../Logger') -const User = require('../objects/User') +const User = require('../objects/user/User') const { getId } = require('../utils/index') diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 8af878ca..ac38e5a0 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -7,7 +7,7 @@ const { getId, secondsToTimestamp } = require('../utils/index') const { writeConcatFile } = require('../utils/ffmpegHelpers') const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator') -const UserListeningSession = require('./UserListeningSession') +const UserListeningSession = require('./legacy/UserListeningSession') class Stream extends EventEmitter { constructor(streamPath, client, libraryItem, transcodeOptions = {}) { diff --git a/server/objects/UserAudiobookData.js b/server/objects/legacy/UserAudiobookData.js similarity index 97% rename from server/objects/UserAudiobookData.js rename to server/objects/legacy/UserAudiobookData.js index b8e45010..41b88e91 100644 --- a/server/objects/UserAudiobookData.js +++ b/server/objects/legacy/UserAudiobookData.js @@ -1,5 +1,5 @@ -const Logger = require('../Logger') -const AudioBookmark = require('./AudioBookmark') +const Logger = require('../../Logger') +const AudioBookmark = require('../user/AudioBookmark') class UserAudiobookData { constructor(progress) { diff --git a/server/objects/UserListeningSession.js b/server/objects/legacy/UserListeningSession.js similarity index 96% rename from server/objects/UserListeningSession.js rename to server/objects/legacy/UserListeningSession.js index 7b07f311..98edaa65 100644 --- a/server/objects/UserListeningSession.js +++ b/server/objects/legacy/UserListeningSession.js @@ -1,6 +1,6 @@ -const Logger = require('../Logger') +const Logger = require('../../Logger') const date = require('date-and-time') -const { getId } = require('../utils/index') +const { getId } = require('../../utils/index') class UserListeningSession { constructor(session) { diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js index 95d2a5ca..c7a64551 100644 --- a/server/objects/metadata/BookMetadata.js +++ b/server/objects/metadata/BookMetadata.js @@ -46,7 +46,7 @@ class BookMetadata { subtitle: this.subtitle, authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id narrators: [...this.narrators], - series: this.series.map(s => ({ ...s })), + series: this.series.map(s => ({ ...s })), // Series JSONMinimal with name, id and sequence genres: [...this.genres], publishedYear: this.publishedYear, publishedDate: this.publishedDate, @@ -80,6 +80,10 @@ class BookMetadata { } } + clone() { + return new BookMetadata(this.toJSON()) + } + get titleIgnorePrefix() { if (!this.title) return '' if (this.title.toLowerCase().startsWith('the ')) { diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js index 7d63bc83..e6e2a74d 100644 --- a/server/objects/metadata/PodcastMetadata.js +++ b/server/objects/metadata/PodcastMetadata.js @@ -48,6 +48,10 @@ class PodcastMetadata { return this.toJSON() } + clone() { + return new PodcastMetadata(this.toJSON()) + } + searchQuery(query) { // Returns key if match is found var keysToCheck = ['title', 'artist', 'itunesId', 'itunesArtistId'] for (var key of keysToCheck) { diff --git a/server/objects/AudioBookmark.js b/server/objects/user/AudioBookmark.js similarity index 73% rename from server/objects/AudioBookmark.js rename to server/objects/user/AudioBookmark.js index 597b7b71..98599a8d 100644 --- a/server/objects/AudioBookmark.js +++ b/server/objects/user/AudioBookmark.js @@ -1,5 +1,6 @@ class AudioBookmark { constructor(bookmark) { + this.libraryItemId = null this.title = null this.time = null this.createdAt = null @@ -11,6 +12,7 @@ class AudioBookmark { toJSON() { return { + libraryItemId: this.libraryItemId, title: this.title || '', time: this.time, createdAt: this.createdAt @@ -18,12 +20,14 @@ class AudioBookmark { } construct(bookmark) { + this.libraryItemId = bookmark.libraryItemId this.title = bookmark.title || '' this.time = bookmark.time || 0 this.createdAt = bookmark.createdAt } - setData(time, title) { + setData(libraryItemId, time, title) { + this.libraryItemId = libraryItemId this.title = title this.time = time this.createdAt = Date.now() diff --git a/server/objects/user/LibraryItemProgress.js b/server/objects/user/LibraryItemProgress.js new file mode 100644 index 00000000..9455a8dd --- /dev/null +++ b/server/objects/user/LibraryItemProgress.js @@ -0,0 +1,99 @@ +const Logger = require('../../Logger') + +class LibraryItemProgress { + constructor(progress) { + this.id = null // Same as library item id + this.libararyItemId = 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 + + if (progress) { + this.construct(progress) + } + } + + toJSON() { + return { + id: this.id, + libararyItemId: this.libararyItemId, + totalDuration: this.totalDuration, + progress: this.progress, + currentTime: this.currentTime, + isRead: this.isRead, + lastUpdate: this.lastUpdate, + startedAt: this.startedAt, + finishedAt: this.finishedAt + } + } + + construct(progress) { + this.id = progress.id + this.libararyItemId = progress.libararyItemId + 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 + } + + 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 + } +} +module.exports = LibraryItemProgress \ No newline at end of file diff --git a/server/objects/user/PlaybackSession.js b/server/objects/user/PlaybackSession.js new file mode 100644 index 00000000..159cf378 --- /dev/null +++ b/server/objects/user/PlaybackSession.js @@ -0,0 +1,103 @@ +const date = require('date-and-time') +const { getId } = require('../../utils/index') +const { PlayMethod } = require('../../utils/constants') +const BookMetadata = require('../metadata/BookMetadata') +const PodcastMetadata = require('../metadata/PodcastMetadata') + +class PlaybackSession { + constructor(session) { + this.id = null + this.userId = null + this.libraryItemId = null + this.mediaType = null + this.mediaMetadata = null + + this.playMethod = null + + this.date = null + this.dayOfWeek = null + + this.timeListening = null + this.startedAt = null + this.updatedAt = null + + if (session) { + this.construct(session) + } + } + + toJSON() { + return { + id: this.id, + sessionType: this.sessionType, + userId: this.userId, + libraryItemId: this.libraryItemId, + mediaType: this.mediaType, + mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null, + playMethod: this.playMethod, + date: this.date, + dayOfWeek: this.dayOfWeek, + timeListening: this.timeListening, + lastUpdate: this.lastUpdate, + updatedAt: this.updatedAt + } + } + + construct(session) { + this.id = session.id + this.sessionType = session.sessionType + this.userId = session.userId + this.libraryItemId = session.libraryItemId + this.mediaType = session.mediaType + this.playMethod = session.playMethod + + this.mediaMetadata = null + if (session.mediaMetadata) { + if (this.mediaType === 'book') { + this.mediaMetadata = new BookMetadata(session.mediaMetadata) + } else if (this.mediaType === 'podcast') { + this.mediaMetadata = new PodcastMetadata(session.mediaMetadata) + } + } + + this.date = session.date + this.dayOfWeek = session.dayOfWeek + + this.timeListening = session.timeListening || null + this.startedAt = session.startedAt + this.updatedAt = session.updatedAt || null + } + + setData(libraryItem, user) { + this.id = getId('ls') + this.userId = user.id + this.libraryItemId = libraryItem.id + this.mediaType = libraryItem.mediaType + this.mediaMetadata = libraryItem.media.metadata.clone() + this.playMethod = PlayMethod.TRANSCODE + + this.timeListening = 0 + this.startedAt = Date.now() + this.updatedAt = 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.updatedAt = 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 = PlaybackSession \ No newline at end of file diff --git a/server/objects/User.js b/server/objects/user/User.js similarity index 65% rename from server/objects/User.js rename to server/objects/user/User.js index 6edc5b41..e4ce5238 100644 --- a/server/objects/User.js +++ b/server/objects/user/User.js @@ -1,5 +1,7 @@ -const Logger = require('../Logger') -const UserAudiobookData = require('./UserAudiobookData') +const Logger = require('../../Logger') +const { isObject } = require('../../utils') +const AudioBookmark = require('./AudioBookmark') +const LibraryItemProgress = require('./LibraryItemProgress') class User { constructor(user) { @@ -13,7 +15,9 @@ class User { this.isLocked = false this.lastSeen = null this.createdAt = null - this.audiobooks = null + + this.libraryItemProgress = [] + this.bookmarks = [] this.settings = {} this.permissions = {} @@ -70,17 +74,6 @@ class User { } } - audiobooksToJSON() { - if (!this.audiobooks) return null - var _map = {} - for (const key in this.audiobooks) { - if (this.audiobooks[key]) { - _map[key] = this.audiobooks[key].toJSON() - } - } - return _map - } - toJSON() { return { id: this.id, @@ -89,7 +82,8 @@ class User { type: this.type, stream: this.stream, token: this.token, - audiobooks: this.audiobooksToJSON(), + libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [], + bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, lastSeen: this.lastSeen, @@ -107,7 +101,7 @@ class User { type: this.type, stream: this.stream, token: this.token, - audiobooks: this.audiobooksToJSON(), + libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, lastSeen: this.lastSeen, @@ -138,16 +132,17 @@ class User { this.type = user.type this.stream = user.stream || null this.token = user.token - if (user.audiobooks) { - this.audiobooks = {} - for (const key in user.audiobooks) { - if (key === '[object Object]') { // TEMP: Bug remove bad data - Logger.warn('[User] Construct found invalid UAD') - } else if (user.audiobooks[key]) { - this.audiobooks[key] = new UserAudiobookData(user.audiobooks[key]) - } - } + + this.libraryItemProgress = [] + if (user.libraryItemProgress) { + this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li)) } + + this.bookmarks = [] + if (user.bookmarks) { + this.bookmarks = user.bookmarks.map(bm => new AudioBookmark(bm)) + } + this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive this.isLocked = user.type === 'root' ? false : !!user.isLocked this.lastSeen = user.lastSeen || null @@ -202,26 +197,26 @@ class User { } updateAudiobookProgressFromStream(stream) { - if (!this.audiobooks) this.audiobooks = {} - if (!this.audiobooks[stream.audiobookId]) { - this.audiobooks[stream.audiobookId] = new UserAudiobookData() - } - this.audiobooks[stream.audiobookId].updateProgressFromStream(stream) - return this.audiobooks[stream.audiobookId] + // if (!this.audiobooks) this.audiobooks = {} + // if (!this.audiobooks[stream.audiobookId]) { + // this.audiobooks[stream.audiobookId] = new UserAudiobookData() + // } + // this.audiobooks[stream.audiobookId].updateProgressFromStream(stream) + // return this.audiobooks[stream.audiobookId] } updateAudiobookData(audiobookId, updatePayload) { - if (!this.audiobooks) this.audiobooks = {} - if (!this.audiobooks[audiobookId]) { - this.audiobooks[audiobookId] = new UserAudiobookData() - this.audiobooks[audiobookId].audiobookId = audiobookId - } - var wasUpdated = this.audiobooks[audiobookId].update(updatePayload) - if (wasUpdated) { - // Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobookId])}`) - return this.audiobooks[audiobookId] - } - return false + // if (!this.audiobooks) this.audiobooks = {} + // if (!this.audiobooks[audiobookId]) { + // this.audiobooks[audiobookId] = new UserAudiobookData() + // this.audiobooks[audiobookId].audiobookId = audiobookId + // } + // var wasUpdated = this.audiobooks[audiobookId].update(updatePayload) + // if (wasUpdated) { + // // Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobookId])}`) + // return this.audiobooks[audiobookId] + // } + // return false } // Returns Boolean If update was made @@ -251,25 +246,25 @@ class User { } resetAudiobookProgress(libraryItem) { - if (!this.audiobooks || !this.audiobooks[libraryItem.id]) { - return false - } - return this.updateAudiobookData(libraryItem.id, { - progress: 0, - currentTime: 0, - isRead: false, - lastUpdate: Date.now(), - startedAt: null, - finishedAt: null - }) + // if (!this.audiobooks || !this.audiobooks[libraryItem.id]) { + // return false + // } + // return this.updateAudiobookData(libraryItem.id, { + // progress: 0, + // currentTime: 0, + // isRead: false, + // lastUpdate: Date.now(), + // startedAt: null, + // finishedAt: null + // }) } deleteAudiobookData(audiobookId) { - if (!this.audiobooks || !this.audiobooks[audiobookId]) { - return false - } - delete this.audiobooks[audiobookId] - return true + // if (!this.audiobooks || !this.audiobooks[audiobookId]) { + // return false + // } + // delete this.audiobooks[audiobookId] + // return true } checkCanAccessLibrary(libraryId) { @@ -278,59 +273,60 @@ class User { return this.librariesAccessible.includes(libraryId) } - getAudiobookJSON(audiobookId) { - if (!this.audiobooks) return null - return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null + getLibraryItemProgress(libraryItemId) { + if (!this.libraryItemProgress) return null + var progress = this.libraryItemProgress.find(lip => lip.id === libraryItemId) + return progress ? progress.toJSON() : null } - createBookmark({ audiobookId, time, title }) { - if (!this.audiobooks) this.audiobooks = {} - if (!this.audiobooks[audiobookId]) { - this.audiobooks[audiobookId] = new UserAudiobookData() - this.audiobooks[audiobookId].audiobookId = audiobookId - } - if (this.audiobooks[audiobookId].checkBookmarkExists(time)) { - return { - error: 'Bookmark already exists' - } - } + createBookmark({ libraryItemId, time, title }) { + // if (!this.audiobooks) this.audiobooks = {} + // if (!this.audiobooks[audiobookId]) { + // this.audiobooks[audiobookId] = new UserAudiobookData() + // this.audiobooks[audiobookId].audiobookId = audiobookId + // } + // if (this.audiobooks[audiobookId].checkBookmarkExists(time)) { + // return { + // error: 'Bookmark already exists' + // } + // } - var success = this.audiobooks[audiobookId].createBookmark(time, title) - if (success) return this.audiobooks[audiobookId] - return null + // var success = this.audiobooks[audiobookId].createBookmark(time, title) + // if (success) return this.audiobooks[audiobookId] + // return null } updateBookmark({ audiobookId, time, title }) { - if (!this.audiobooks || !this.audiobooks[audiobookId]) { - return { - error: 'Invalid Audiobook' - } - } - if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { - return { - error: 'Bookmark does not exist' - } - } + // if (!this.audiobooks || !this.audiobooks[audiobookId]) { + // return { + // error: 'Invalid Audiobook' + // } + // } + // if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { + // return { + // error: 'Bookmark does not exist' + // } + // } - var success = this.audiobooks[audiobookId].updateBookmark(time, title) - if (success) return this.audiobooks[audiobookId] - return null + // var success = this.audiobooks[audiobookId].updateBookmark(time, title) + // if (success) return this.audiobooks[audiobookId] + // return null } deleteBookmark({ audiobookId, time }) { - if (!this.audiobooks || !this.audiobooks[audiobookId]) { - return { - error: 'Invalid Audiobook' - } - } - if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { - return { - error: 'Bookmark does not exist' - } - } + // if (!this.audiobooks || !this.audiobooks[audiobookId]) { + // return { + // error: 'Invalid Audiobook' + // } + // } + // if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { + // return { + // error: 'Bookmark does not exist' + // } + // } - this.audiobooks[audiobookId].deleteBookmark(time) - return this.audiobooks[audiobookId] + // this.audiobooks[audiobookId].deleteBookmark(time) + // return this.audiobooks[audiobookId] } syncLocalUserAudiobookData(localUserAudiobookData, audiobook) { diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 72033689..45287da6 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -646,31 +646,6 @@ class Scanner { } } - // TEMP: Old version created ids that had a chance of repeating - async fixDuplicateIds() { - var ids = {} - var audiobooksUpdated = 0 - for (let i = 0; i < this.db.audiobooks.length; i++) { - var ab = this.db.audiobooks[i] - if (ids[ab.id]) { - var abCopy = new Audiobook(ab.toJSON()) - abCopy.id = getId('ab') - if (abCopy.book.cover) { - abCopy.book.cover = abCopy.book.cover.replace(ab.id, abCopy.id) - } - Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id) - await this.db.removeEntity('audiobook', ab.id) - await this.db.insertAudiobook(abCopy) - audiobooksUpdated++ - } else { - ids[ab.id] = true - } - } - if (audiobooksUpdated) { - Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`) - } - } - async quickMatchBook(libraryItem, options = {}) { var provider = options.provider || 'google' var searchTitle = options.title || libraryItem.media.metadata.title diff --git a/server/utils/constants.js b/server/utils/constants.js index a114794d..fc79b872 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -25,3 +25,9 @@ module.exports.LogLevel = { FATAL: 5, NOTE: 6 } + +module.exports.PlayMethod = { + DIRECTPLAY: 0, + DIRECTSTREAM: 1, + TRANSCODE: 2 +} \ No newline at end of file diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js index 574cc289..5ef8b222 100644 --- a/server/utils/dbMigration.js +++ b/server/utils/dbMigration.js @@ -4,6 +4,8 @@ const njodb = require("njodb") const { SupportedEbookTypes } = require('./globals') const Audiobook = require('../objects/legacy/Audiobook') +const UserAudiobookData = require('../objects/legacy/UserAudiobookData') + const LibraryItem = require('../objects/LibraryItem') const Logger = require('../Logger') @@ -16,6 +18,11 @@ const EBookFile = require('../objects/files/EBookFile') const LibraryFile = require('../objects/files/LibraryFile') const FileMetadata = require('../objects/metadata/FileMetadata') const AudioMetaTags = require('../objects/metadata/AudioMetaTags') +const LibraryItemProgress = require('../objects/user/LibraryItemProgress') +const PlaybackSession = require('../objects/user/PlaybackSession') + +const { isObject } = require('.') +const User = require('../objects/user/User') var authorsToAdd = [] var existingDbAuthors = [] @@ -184,8 +191,8 @@ function makeLibraryItemFromOldAb(audiobook) { return libraryItem } -async function migrateDb(db) { - Logger.info(`==== Starting DB Migration ====`) +async function migrateLibraryItems(db) { + Logger.info(`==== Starting Library Item migration ====`) var audiobooks = await loadAudiobooks() if (!audiobooks.length) { @@ -223,6 +230,114 @@ async function migrateDb(db) { existingDbAuthors = [] authorsToAdd = [] seriesToAdd = [] - Logger.info(`==== DB Migration Complete ====`) + Logger.info(`==== Library Item migration complete ====`) } -module.exports = migrateDb \ No newline at end of file +module.exports.migrateLibraryItems = migrateLibraryItems + +function cleanUserObject(db, userObj) { + + var cleanedUserPayload = { + ...userObj, + libraryItemProgress: [], + bookmarks: [] + } + + // UserAudiobookData is now LibraryItemProgress and AudioBookmarks separated + if (userObj.audiobooks) { + for (const audiobookId in userObj.audiobooks) { + if (isObject(userObj.audiobooks[audiobookId])) { + // Bookmarks now live on User.js object instead of inside UserAudiobookData + if (userObj.audiobooks[audiobookId].bookmarks) { + const cleanedBookmarks = userObj.audiobooks[audiobookId].bookmarks.map((bm) => { + bm.libraryItemId = audiobookId + return bm + }) + cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks) + } + + var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object + var liProgress = new LibraryItemProgress() // New Progress Object + liProgress.id = userAudiobookData.audiobookId + liProgress.libraryItemId = userAudiobookData.audiobookId + Object.keys(liProgress.toJSON()).forEach((key) => { + if (userAudiobookData[key] !== undefined) { + liProgress[key] = userAudiobookData[key] + } + }) + cleanedUserPayload.libraryItemProgress.push(liProgress.toJSON()) + } + } + } + + const user = new User(cleanedUserPayload) + return db.usersDb.update((record) => record.id === user.id, () => user).then((results) => { + Logger.debug(`[dbMigration] Updated User: ${results.updated} | Selected: ${results.selected}`) + return true + }).catch((error) => { + Logger.error(`[dbMigration] Update User Failed: ${error}`) + return false + }) +} + +function cleanSessionObj(db, userListeningSession) { + var newPlaybackSession = new PlaybackSession(userListeningSession) + newPlaybackSession.mediaType = 'book' + newPlaybackSession.updatedAt = userListeningSession.lastUpdate + newPlaybackSession.libraryItemId = userListeningSession.audiobookId + + // We only have title to transfer over nicely + var bookMetadata = new BookMetadata() + bookMetadata.title = userListeningSession.audiobookTitle || '' + newPlaybackSession.mediaMetadata = bookMetadata + + return db.sessionsDb.update((record) => record.id === newPlaybackSession.id, () => newPlaybackSession).then((results) => true).catch((error) => { + Logger.error(`[dbMigration] Update Session Failed: ${error}`) + return false + }) +} + +async function migrateUserData(db) { + Logger.info(`==== Starting User migration ====`) + + const userObjects = await db.usersDb.select((result) => result.audiobooks != undefined).then((results) => results.data) + if (!userObjects.length) { + Logger.warn('[dbMigration] No users found needing migration') + return + } + + var userCount = 0 + for (const userObj of userObjects) { + Logger.info(`[dbMigration] Migrating User "${userObj.username}"`) + var success = await cleanUserObject(db, userObj) + if (!success) { + await new Promise((resolve) => setTimeout(resolve, 500)) + Logger.warn(`[dbMigration] Second attempt Migrating User "${userObj.username}"`) + success = await cleanUserObject(db, userObj) + if (!success) { + throw new Error('Db migration failed migrating users') + } + } + userCount++ + } + + var sessionCount = 0 + const userListeningSessions = await db.sessionsDb.select((result) => result.audiobookId != undefined).then((results) => results.data) + if (userListeningSessions.length) { + + for (const session of userListeningSessions) { + var success = await cleanSessionObj(db, session) + if (!success) { + await new Promise((resolve) => setTimeout(resolve, 500)) + Logger.warn(`[dbMigration] Second attempt Migrating Session "${session.id}"`) + success = await cleanSessionObj(db, session) + if (!success) { + Logger.error(`[dbMigration] Failed to migrate session "${session.id}"`) + } + } + if (success) sessionCount++ + } + } + + Logger.info(`==== User migration complete (${userCount} Users, ${sessionCount} Sessions) ====`) +} +module.exports.migrateUserData = migrateUserData \ No newline at end of file diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index b80bda34..85e98f7d 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -28,7 +28,7 @@ module.exports = { else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter)) else if (group === 'progress') { filtered = filtered.filter(li => { - var userAudiobook = user.getAudiobookJSON(li.id) + var userAudiobook = user.getLibraryItemProgress(li.id) var isRead = userAudiobook && userAudiobook.isRead if (filter === 'Read' && isRead) return true if (filter === 'Unread' && !isRead) return true @@ -67,7 +67,7 @@ module.exports = { else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter)) else if (group === 'progress') { filtered = filtered.filter(ab => { - var userAudiobook = user.getAudiobookJSON(ab.id) + var userAudiobook = user.getLibraryItemProgress(ab.id) var isRead = userAudiobook && userAudiobook.isRead if (filter === 'Read' && isRead) return true if (filter === 'Unread' && !isRead) return true @@ -163,7 +163,7 @@ module.exports = { var _series = {} books.forEach((audiobook) => { if (audiobook.book.series) { - var bookWithUserAb = { userAudiobook: user.getAudiobookJSON(audiobook.id), book: audiobook } + var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook } if (!_series[audiobook.book.series]) { _series[audiobook.book.series] = { id: audiobook.book.series, @@ -197,7 +197,7 @@ module.exports = { getBooksWithUserAudiobook(user, books) { return books.map(book => { return { - userAudiobook: user.getAudiobookJSON(book.id), + userAudiobook: user.getLibraryItemProgress(book.id), book } }).filter(b => !!b.userAudiobook)