mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 02:17:01 -04:00 
			
		
		
		
	Init sqlite take 2
This commit is contained in:
		
							parent
							
								
									d86a3b3dc2
								
							
						
					
					
						commit
						cf7fd315b6
					
				| @ -154,7 +154,7 @@ export default { | ||||
|       } | ||||
|       this.deletingDeviceName = device.name | ||||
|       this.$axios | ||||
|         .$patch(`/emails/ereader-devices`, payload) | ||||
|         .$post(`/api/emails/ereader-devices`, payload) | ||||
|         .then((data) => { | ||||
|           this.ereaderDevicesUpdated(data.ereaderDevices) | ||||
|           this.$toast.success('Device deleted') | ||||
|  | ||||
| @ -191,6 +191,7 @@ export default class PlayerHandler { | ||||
| 
 | ||||
|     const payload = { | ||||
|       deviceInfo: { | ||||
|         clientName: 'Abs Web', | ||||
|         deviceId: this.getDeviceId() | ||||
|       }, | ||||
|       supportedMimeTypes: this.player.playableMimeTypes, | ||||
|  | ||||
							
								
								
									
										2376
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2376
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -36,7 +36,9 @@ | ||||
|     "htmlparser2": "^8.0.1", | ||||
|     "node-tone": "^1.0.1", | ||||
|     "nodemailer": "^6.9.2", | ||||
|     "sequelize": "^6.32.1", | ||||
|     "socket.io": "^4.5.4", | ||||
|     "sqlite3": "^5.1.6", | ||||
|     "xml2js": "^0.5.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | ||||
| @ -2,21 +2,10 @@ const bcrypt = require('./libs/bcryptjs') | ||||
| const jwt = require('./libs/jsonwebtoken') | ||||
| const requestIp = require('./libs/requestIp') | ||||
| const Logger = require('./Logger') | ||||
| const Database = require('./Database') | ||||
| 
 | ||||
| class Auth { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
| 
 | ||||
|     this.user = null | ||||
|   } | ||||
| 
 | ||||
|   get username() { | ||||
|     return this.user ? this.user.username : 'nobody' | ||||
|   } | ||||
| 
 | ||||
|   get users() { | ||||
|     return this.db.users | ||||
|   } | ||||
|   constructor() { } | ||||
| 
 | ||||
|   cors(req, res, next) { | ||||
|     res.header('Access-Control-Allow-Origin', '*') | ||||
| @ -35,20 +24,20 @@ class Auth { | ||||
|   async initTokenSecret() { | ||||
|     if (process.env.TOKEN_SECRET) { // User can supply their own token secret
 | ||||
|       Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`) | ||||
|       this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET | ||||
|       Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET | ||||
|     } else { | ||||
|       Logger.debug(`[Auth] Setting token secret - using random bytes`) | ||||
|       this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') | ||||
|       Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64') | ||||
|     } | ||||
|     await this.db.updateServerSettings() | ||||
|     await Database.updateServerSettings() | ||||
| 
 | ||||
|     // New token secret creation added in v2.1.0 so generate new API tokens for each user
 | ||||
|     if (this.db.users.length) { | ||||
|       for (const user of this.db.users) { | ||||
|     if (Database.users.length) { | ||||
|       for (const user of Database.users) { | ||||
|         user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) | ||||
|         Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`) | ||||
|       } | ||||
|       await this.db.updateEntities('user', this.db.users) | ||||
|       await Database.updateBulkUsers(Database.users) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -68,7 +57,7 @@ class Auth { | ||||
|       return res.sendStatus(401) | ||||
|     } | ||||
| 
 | ||||
|     var user = await this.verifyToken(token) | ||||
|     const user = await this.verifyToken(token) | ||||
|     if (!user) { | ||||
|       Logger.error('Verify Token User Not Found', token) | ||||
|       return res.sendStatus(404) | ||||
| @ -95,7 +84,7 @@ class Auth { | ||||
|   } | ||||
| 
 | ||||
|   generateAccessToken(payload) { | ||||
|     return jwt.sign(payload, global.ServerSettings.tokenSecret); | ||||
|     return jwt.sign(payload, Database.serverSettings.tokenSecret) | ||||
|   } | ||||
| 
 | ||||
|   authenticateUser(token) { | ||||
| @ -104,12 +93,12 @@ class Auth { | ||||
| 
 | ||||
|   verifyToken(token) { | ||||
|     return new Promise((resolve) => { | ||||
|       jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => { | ||||
|       jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => { | ||||
|         if (!payload || err) { | ||||
|           Logger.error('JWT Verify Token Failed', err) | ||||
|           return resolve(null) | ||||
|         } | ||||
|         const user = this.users.find(u => u.id === payload.userId && u.username === payload.username) | ||||
|         const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username) | ||||
|         resolve(user || null) | ||||
|       }) | ||||
|     }) | ||||
| @ -118,9 +107,9 @@ class Auth { | ||||
|   getUserLoginResponsePayload(user) { | ||||
|     return { | ||||
|       user: user.toJSONForBrowser(), | ||||
|       userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), | ||||
|       serverSettings: this.db.serverSettings.toJSONForBrowser(), | ||||
|       ereaderDevices: this.db.emailSettings.getEReaderDevices(user), | ||||
|       userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries), | ||||
|       serverSettings: Database.serverSettings.toJSONForBrowser(), | ||||
|       ereaderDevices: Database.emailSettings.getEReaderDevices(user), | ||||
|       Source: global.Source | ||||
|     } | ||||
|   } | ||||
| @ -130,7 +119,7 @@ class Auth { | ||||
|     const username = (req.body.username || '').toLowerCase() | ||||
|     const password = req.body.password || '' | ||||
| 
 | ||||
|     const user = this.users.find(u => u.username.toLowerCase() === username) | ||||
|     const user = Database.users.find(u => u.username.toLowerCase() === username) | ||||
| 
 | ||||
|     if (!user?.isActive) { | ||||
|       Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) | ||||
| @ -142,7 +131,7 @@ class Auth { | ||||
|     } | ||||
| 
 | ||||
|     // Check passwordless root user
 | ||||
|     if (user.id === 'root' && (!user.pash || user.pash === '')) { | ||||
|     if (user.type === 'root' && (!user.pash || user.pash === '')) { | ||||
|       if (password) { | ||||
|         return res.status(401).send('Invalid root password (hint: there is none)') | ||||
|       } else { | ||||
| @ -166,15 +155,6 @@ class Auth { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Not in use now
 | ||||
|   lockUser(user) { | ||||
|     user.isLocked = true | ||||
|     return this.db.updateEntity('user', user).catch((error) => { | ||||
|       Logger.error('[Auth] Failed to lock user', user.username, error) | ||||
|       return false | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   comparePassword(password, user) { | ||||
|     if (user.type === 'root' && !password && !user.pash) return true | ||||
|     if (!password || !user.pash) return false | ||||
| @ -184,7 +164,7 @@ class Auth { | ||||
|   async userChangePassword(req, res) { | ||||
|     var { password, newPassword } = req.body | ||||
|     newPassword = newPassword || '' | ||||
|     var matchingUser = this.users.find(u => u.id === req.user.id) | ||||
|     const matchingUser = Database.users.find(u => u.id === req.user.id) | ||||
| 
 | ||||
|     // Only root can have an empty password
 | ||||
|     if (matchingUser.type !== 'root' && !newPassword) { | ||||
| @ -193,14 +173,14 @@ class Auth { | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     var compare = await this.comparePassword(password, matchingUser) | ||||
|     const compare = await this.comparePassword(password, matchingUser) | ||||
|     if (!compare) { | ||||
|       return res.json({ | ||||
|         error: 'Invalid password' | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     var pw = '' | ||||
|     let pw = '' | ||||
|     if (newPassword) { | ||||
|       pw = await this.hashPass(newPassword) | ||||
|       if (!pw) { | ||||
| @ -211,7 +191,8 @@ class Auth { | ||||
|     } | ||||
| 
 | ||||
|     matchingUser.pash = pw | ||||
|     var success = await this.db.updateEntity('user', matchingUser) | ||||
| 
 | ||||
|     const success = await Database.updateUser(matchingUser) | ||||
|     if (success) { | ||||
|       res.json({ | ||||
|         success: true | ||||
|  | ||||
							
								
								
									
										456
									
								
								server/Database.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										456
									
								
								server/Database.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,456 @@ | ||||
| const Path = require('path') | ||||
| const { Sequelize } = require('sequelize') | ||||
| 
 | ||||
| const packageJson = require('../package.json') | ||||
| const fs = require('./libs/fsExtra') | ||||
| const Logger = require('./Logger') | ||||
| 
 | ||||
| const dbMigration = require('./utils/migrations/dbMigration') | ||||
| 
 | ||||
| class Database { | ||||
|   constructor() { | ||||
|     this.sequelize = null | ||||
|     this.dbPath = null | ||||
|     this.isNew = false // New database.sqlite created
 | ||||
| 
 | ||||
|     // Temporarily using format of old DB
 | ||||
|     // below data should be loaded from the DB as needed
 | ||||
|     this.libraryItems = [] | ||||
|     this.users = [] | ||||
|     this.libraries = [] | ||||
|     this.settings = [] | ||||
|     this.collections = [] | ||||
|     this.playlists = [] | ||||
|     this.authors = [] | ||||
|     this.series = [] | ||||
|     this.feeds = [] | ||||
| 
 | ||||
|     this.serverSettings = null | ||||
|     this.notificationSettings = null | ||||
|     this.emailSettings = null | ||||
|   } | ||||
| 
 | ||||
|   get models() { | ||||
|     return this.sequelize?.models || {} | ||||
|   } | ||||
| 
 | ||||
|   get hasRootUser() { | ||||
|     return this.users.some(u => u.type === 'root') | ||||
|   } | ||||
| 
 | ||||
|   async checkHasDb() { | ||||
|     if (!await fs.pathExists(this.dbPath)) { | ||||
|       Logger.info(`[Database] database.sqlite not found at ${this.dbPath}`) | ||||
|       return false | ||||
|     } | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   async init(force = false) { | ||||
|     this.dbPath = Path.join(global.ConfigPath, 'database.sqlite') | ||||
| 
 | ||||
|     // First check if this is a new database
 | ||||
|     this.isNew = !(await this.checkHasDb()) || force | ||||
| 
 | ||||
|     if (!await this.connect()) { | ||||
|       throw new Error('Database connection failed') | ||||
|     } | ||||
| 
 | ||||
|     await this.buildModels(force) | ||||
|     Logger.info(`[Database] Db initialized`, Object.keys(this.sequelize.models)) | ||||
| 
 | ||||
|     await this.loadData(force) | ||||
|   } | ||||
| 
 | ||||
|   async connect() { | ||||
|     Logger.info(`[Database] Initializing db at "${this.dbPath}"`) | ||||
|     this.sequelize = new Sequelize({ | ||||
|       dialect: 'sqlite', | ||||
|       storage: this.dbPath, | ||||
|       logging: false | ||||
|     }) | ||||
| 
 | ||||
|     // Helper function
 | ||||
|     this.sequelize.uppercaseFirst = str => str ? `${str[0].toUpperCase()}${str.substr(1)}` : '' | ||||
| 
 | ||||
|     try { | ||||
|       await this.sequelize.authenticate() | ||||
|       Logger.info(`[Database] Db connection was successful`) | ||||
|       return true | ||||
|     } catch (error) { | ||||
|       Logger.error(`[Database] Failed to connect to db`, error) | ||||
|       return false | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   buildModels(force = false) { | ||||
|     require('./models/User')(this.sequelize) | ||||
|     require('./models/Library')(this.sequelize) | ||||
|     require('./models/LibraryFolder')(this.sequelize) | ||||
|     require('./models/Book')(this.sequelize) | ||||
|     require('./models/Podcast')(this.sequelize) | ||||
|     require('./models/PodcastEpisode')(this.sequelize) | ||||
|     require('./models/LibraryItem')(this.sequelize) | ||||
|     require('./models/MediaProgress')(this.sequelize) | ||||
|     require('./models/Series')(this.sequelize) | ||||
|     require('./models/BookSeries')(this.sequelize) | ||||
|     require('./models/Author')(this.sequelize) | ||||
|     require('./models/BookAuthor')(this.sequelize) | ||||
|     require('./models/Collection')(this.sequelize) | ||||
|     require('./models/CollectionBook')(this.sequelize) | ||||
|     require('./models/Playlist')(this.sequelize) | ||||
|     require('./models/PlaylistMediaItem')(this.sequelize) | ||||
|     require('./models/Device')(this.sequelize) | ||||
|     require('./models/PlaybackSession')(this.sequelize) | ||||
|     require('./models/Feed')(this.sequelize) | ||||
|     require('./models/FeedEpisode')(this.sequelize) | ||||
|     require('./models/Setting')(this.sequelize) | ||||
| 
 | ||||
|     return this.sequelize.sync({ force }) | ||||
|   } | ||||
| 
 | ||||
|   async loadData(force = false) { | ||||
|     if (this.isNew && await dbMigration.checkShouldMigrate(force)) { | ||||
|       Logger.info(`[Database] New database was created and old database was detected - migrating old to new`) | ||||
|       await dbMigration.migrate(this.models) | ||||
|     } | ||||
| 
 | ||||
|     const startTime = Date.now() | ||||
| 
 | ||||
|     this.libraryItems = await this.models.libraryItem.getAllOldLibraryItems() | ||||
|     this.users = await this.models.user.getOldUsers() | ||||
|     this.libraries = await this.models.library.getAllOldLibraries() | ||||
|     this.collections = await this.models.collection.getOldCollections() | ||||
|     this.playlists = await this.models.playlist.getOldPlaylists() | ||||
|     this.authors = await this.models.author.getOldAuthors() | ||||
|     this.series = await this.models.series.getAllOldSeries() | ||||
|     this.feeds = await this.models.feed.getOldFeeds() | ||||
| 
 | ||||
|     const settingsData = await this.models.setting.getOldSettings() | ||||
|     this.settings = settingsData.settings | ||||
|     this.emailSettings = settingsData.emailSettings | ||||
|     this.serverSettings = settingsData.serverSettings | ||||
|     this.notificationSettings = settingsData.notificationSettings | ||||
|     global.ServerSettings = this.serverSettings.toJSON() | ||||
| 
 | ||||
|     Logger.info(`[Database] Db data loaded in ${Date.now() - startTime}ms`) | ||||
| 
 | ||||
|     if (packageJson.version !== this.serverSettings.version) { | ||||
|       Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`) | ||||
|       this.serverSettings.version = packageJson.version | ||||
|       await this.updateServerSettings() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async createRootUser(username, pash, token) { | ||||
|     const newUser = await this.models.user.createRootUser(username, pash, token) | ||||
|     if (newUser) { | ||||
|       this.users.push(newUser) | ||||
|       return true | ||||
|     } | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   updateServerSettings() { | ||||
|     global.ServerSettings = this.serverSettings.toJSON() | ||||
|     return this.updateSetting(this.serverSettings) | ||||
|   } | ||||
| 
 | ||||
|   updateSetting(settings) { | ||||
|     return this.models.setting.updateSettingObj(settings.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   async createUser(oldUser) { | ||||
|     await this.models.user.createFromOld(oldUser) | ||||
|     this.users.push(oldUser) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   updateUser(oldUser) { | ||||
|     return this.models.user.updateFromOld(oldUser) | ||||
|   } | ||||
| 
 | ||||
|   updateBulkUsers(oldUsers) { | ||||
|     return Promise.all(oldUsers.map(u => this.updateUser(u))) | ||||
|   } | ||||
| 
 | ||||
|   async removeUser(userId) { | ||||
|     await this.models.user.removeById(userId) | ||||
|     this.users = this.users.filter(u => u.id !== userId) | ||||
|   } | ||||
| 
 | ||||
|   upsertMediaProgress(oldMediaProgress) { | ||||
|     return this.models.mediaProgress.upsertFromOld(oldMediaProgress) | ||||
|   } | ||||
| 
 | ||||
|   removeMediaProgress(mediaProgressId) { | ||||
|     return this.models.mediaProgress.removeById(mediaProgressId) | ||||
|   } | ||||
| 
 | ||||
|   updateBulkBooks(oldBooks) { | ||||
|     return Promise.all(oldBooks.map(oldBook => this.models.book.saveFromOld(oldBook))) | ||||
|   } | ||||
| 
 | ||||
|   async createLibrary(oldLibrary) { | ||||
|     await this.models.library.createFromOld(oldLibrary) | ||||
|     this.libraries.push(oldLibrary) | ||||
|   } | ||||
| 
 | ||||
|   updateLibrary(oldLibrary) { | ||||
|     return this.models.library.updateFromOld(oldLibrary) | ||||
|   } | ||||
| 
 | ||||
|   async removeLibrary(libraryId) { | ||||
|     await this.models.library.removeById(libraryId) | ||||
|     this.libraries = this.libraries.filter(lib => lib.id !== libraryId) | ||||
|   } | ||||
| 
 | ||||
|   async createCollection(oldCollection) { | ||||
|     const newCollection = await this.models.collection.createFromOld(oldCollection) | ||||
|     // Create CollectionBooks
 | ||||
|     if (newCollection) { | ||||
|       const collectionBooks = [] | ||||
|       oldCollection.books.forEach((libraryItemId) => { | ||||
|         const libraryItem = this.libraryItems.filter(li => li.id === libraryItemId) | ||||
|         if (libraryItem) { | ||||
|           collectionBooks.push({ | ||||
|             collectionId: newCollection.id, | ||||
|             bookId: libraryItem.media.id | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|       if (collectionBooks.length) { | ||||
|         await this.createBulkCollectionBooks(collectionBooks) | ||||
|       } | ||||
|     } | ||||
|     this.collections.push(oldCollection) | ||||
|   } | ||||
| 
 | ||||
|   updateCollection(oldCollection) { | ||||
|     const collectionBooks = [] | ||||
|     let order = 1 | ||||
|     oldCollection.books.forEach((libraryItemId) => { | ||||
|       const libraryItem = this.getLibraryItem(libraryItemId) | ||||
|       if (!libraryItem) return | ||||
|       collectionBooks.push({ | ||||
|         collectionId: oldCollection.id, | ||||
|         bookId: libraryItem.media.id, | ||||
|         order: order++ | ||||
|       }) | ||||
|     }) | ||||
|     return this.models.collection.fullUpdateFromOld(oldCollection, collectionBooks) | ||||
|   } | ||||
| 
 | ||||
|   async removeCollection(collectionId) { | ||||
|     await this.models.collection.removeById(collectionId) | ||||
|     this.collections = this.collections.filter(c => c.id !== collectionId) | ||||
|   } | ||||
| 
 | ||||
|   createCollectionBook(collectionBook) { | ||||
|     return this.models.collectionBook.create(collectionBook) | ||||
|   } | ||||
| 
 | ||||
|   createBulkCollectionBooks(collectionBooks) { | ||||
|     return this.models.collectionBook.bulkCreate(collectionBooks) | ||||
|   } | ||||
| 
 | ||||
|   removeCollectionBook(collectionId, bookId) { | ||||
|     return this.models.collectionBook.removeByIds(collectionId, bookId) | ||||
|   } | ||||
| 
 | ||||
|   async createPlaylist(oldPlaylist) { | ||||
|     const newPlaylist = await this.models.playlist.createFromOld(oldPlaylist) | ||||
|     if (newPlaylist) { | ||||
|       const playlistMediaItems = [] | ||||
|       let order = 1 | ||||
|       for (const mediaItemObj of oldPlaylist.items) { | ||||
|         const libraryItem = this.libraryItems.find(li => li.id === mediaItemObj.libraryItemId) | ||||
|         if (!libraryItem) continue | ||||
| 
 | ||||
|         let mediaItemId = libraryItem.media.id // bookId
 | ||||
|         let mediaItemType = 'book' | ||||
|         if (mediaItemObj.episodeId) { | ||||
|           mediaItemType = 'podcastEpisode' | ||||
|           mediaItemId = mediaItemObj.episodeId | ||||
|         } | ||||
|         playlistMediaItems.push({ | ||||
|           playlistId: newPlaylist.id, | ||||
|           mediaItemId, | ||||
|           mediaItemType, | ||||
|           order: order++ | ||||
|         }) | ||||
|       } | ||||
|       if (playlistMediaItems.length) { | ||||
|         await this.createBulkPlaylistMediaItems(playlistMediaItems) | ||||
|       } | ||||
|     } | ||||
|     this.playlists.push(oldPlaylist) | ||||
|   } | ||||
| 
 | ||||
|   updatePlaylist(oldPlaylist) { | ||||
|     const playlistMediaItems = [] | ||||
|     let order = 1 | ||||
|     oldPlaylist.items.forEach((item) => { | ||||
|       const libraryItem = this.getLibraryItem(item.libraryItemId) | ||||
|       if (!libraryItem) return | ||||
|       playlistMediaItems.push({ | ||||
|         playlistId: oldPlaylist.id, | ||||
|         mediaItemId: item.episodeId || libraryItem.media.id, | ||||
|         mediaItemType: item.episodeId ? 'podcastEpisode' : 'book', | ||||
|         order: order++ | ||||
|       }) | ||||
|     }) | ||||
|     return this.models.playlist.fullUpdateFromOld(oldPlaylist, playlistMediaItems) | ||||
|   } | ||||
| 
 | ||||
|   async removePlaylist(playlistId) { | ||||
|     await this.models.playlist.removeById(playlistId) | ||||
|     this.playlists = this.playlists.filter(p => p.id !== playlistId) | ||||
|   } | ||||
| 
 | ||||
|   createPlaylistMediaItem(playlistMediaItem) { | ||||
|     return this.models.playlistMediaItem.create(playlistMediaItem) | ||||
|   } | ||||
| 
 | ||||
|   createBulkPlaylistMediaItems(playlistMediaItems) { | ||||
|     return this.models.playlistMediaItem.bulkCreate(playlistMediaItems) | ||||
|   } | ||||
| 
 | ||||
|   removePlaylistMediaItem(playlistId, mediaItemId) { | ||||
|     return this.models.playlistMediaItem.removeByIds(playlistId, mediaItemId) | ||||
|   } | ||||
| 
 | ||||
|   getLibraryItem(libraryItemId) { | ||||
|     return this.libraryItems.find(li => li.id === libraryItemId) | ||||
|   } | ||||
| 
 | ||||
|   async createLibraryItem(oldLibraryItem) { | ||||
|     await this.models.libraryItem.fullCreateFromOld(oldLibraryItem) | ||||
|     this.libraryItems.push(oldLibraryItem) | ||||
|   } | ||||
| 
 | ||||
|   updateLibraryItem(oldLibraryItem) { | ||||
|     return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) | ||||
|   } | ||||
| 
 | ||||
|   async updateBulkLibraryItems(oldLibraryItems) { | ||||
|     let updatesMade = 0 | ||||
|     for (const oldLibraryItem of oldLibraryItems) { | ||||
|       const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem) | ||||
|       if (hasUpdates) updatesMade++ | ||||
|     } | ||||
|     return updatesMade | ||||
|   } | ||||
| 
 | ||||
|   async createBulkLibraryItems(oldLibraryItems) { | ||||
|     for (const oldLibraryItem of oldLibraryItems) { | ||||
|       await this.models.libraryItem.fullCreateFromOld(oldLibraryItem) | ||||
|       this.libraryItems.push(oldLibraryItem) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async removeLibraryItem(libraryItemId) { | ||||
|     await this.models.libraryItem.removeById(libraryItemId) | ||||
|     this.libraryItems = this.libraryItems.filter(li => li.id !== libraryItemId) | ||||
|   } | ||||
| 
 | ||||
|   createFeed(oldFeed) { | ||||
|     // TODO: Implement
 | ||||
|   } | ||||
| 
 | ||||
|   updateFeed(oldFeed) { | ||||
|     // TODO: Implement
 | ||||
|   } | ||||
| 
 | ||||
|   async removeFeed(feedId) { | ||||
|     await this.models.feed.removeById(feedId) | ||||
|     this.feeds = this.feeds.filter(f => f.id !== feedId) | ||||
|   } | ||||
| 
 | ||||
|   updateSeries(oldSeries) { | ||||
|     return this.models.series.updateFromOld(oldSeries) | ||||
|   } | ||||
| 
 | ||||
|   async createSeries(oldSeries) { | ||||
|     await this.models.series.createFromOld(oldSeries) | ||||
|     this.series.push(oldSeries) | ||||
|   } | ||||
| 
 | ||||
|   async createBulkSeries(oldSeriesObjs) { | ||||
|     await this.models.series.createBulkFromOld(oldSeriesObjs) | ||||
|     this.series.push(...oldSeriesObjs) | ||||
|   } | ||||
| 
 | ||||
|   async removeSeries(seriesId) { | ||||
|     await this.models.series.removeById(seriesId) | ||||
|     this.series = this.series.filter(se => se.id !== seriesId) | ||||
|   } | ||||
| 
 | ||||
|   async createAuthor(oldAuthor) { | ||||
|     await this.models.createFromOld(oldAuthor) | ||||
|     this.authors.push(oldAuthor) | ||||
|   } | ||||
| 
 | ||||
|   async createBulkAuthors(oldAuthors) { | ||||
|     await this.models.author.createBulkFromOld(oldAuthors) | ||||
|     this.authors.push(...oldAuthors) | ||||
|   } | ||||
| 
 | ||||
|   updateAuthor(oldAuthor) { | ||||
|     return this.models.author.updateFromOld(oldAuthor) | ||||
|   } | ||||
| 
 | ||||
|   async removeAuthor(authorId) { | ||||
|     await this.models.author.removeById(authorId) | ||||
|     this.authors = this.authors.filter(au => au.id !== authorId) | ||||
|   } | ||||
| 
 | ||||
|   async createBulkBookAuthors(bookAuthors) { | ||||
|     await this.models.bookAuthor.bulkCreate(bookAuthors) | ||||
|     this.authors.push(...bookAuthors) | ||||
|   } | ||||
| 
 | ||||
|   async removeBulkBookAuthors(authorId = null, bookId = null) { | ||||
|     if (!authorId && !bookId) return | ||||
|     await this.models.bookAuthor.removeByIds(authorId, bookId) | ||||
|     this.authors = this.authors.filter(au => { | ||||
|       if (authorId && au.authorId !== authorId) return true | ||||
|       if (bookId && au.bookId !== bookId) return true | ||||
|       return false | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   getPlaybackSessions(where = null) { | ||||
|     return this.models.playbackSession.getOldPlaybackSessions(where) | ||||
|   } | ||||
| 
 | ||||
|   getPlaybackSession(sessionId) { | ||||
|     return this.models.playbackSession.getById(sessionId) | ||||
|   } | ||||
| 
 | ||||
|   createPlaybackSession(oldSession) { | ||||
|     return this.models.playbackSession.createFromOld(oldSession) | ||||
|   } | ||||
| 
 | ||||
|   updatePlaybackSession(oldSession) { | ||||
|     return this.models.playbackSession.updateFromOld(oldSession) | ||||
|   } | ||||
| 
 | ||||
|   removePlaybackSession(sessionId) { | ||||
|     return this.models.playbackSession.removeById(sessionId) | ||||
|   } | ||||
| 
 | ||||
|   getDeviceByDeviceId(deviceId) { | ||||
|     return this.models.device.getOldDeviceByDeviceId(deviceId) | ||||
|   } | ||||
| 
 | ||||
|   updateDevice(oldDevice) { | ||||
|     return this.models.device.updateFromOld(oldDevice) | ||||
|   } | ||||
| 
 | ||||
|   createDevice(oldDevice) { | ||||
|     return this.models.device.createFromOld(oldDevice) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = new Database() | ||||
							
								
								
									
										49
									
								
								server/Db.js
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								server/Db.js
									
									
									
									
									
								
							| @ -243,9 +243,6 @@ class Db { | ||||
|   getLibraryItem(id) { | ||||
|     return this.libraryItems.find(li => li.id === id) | ||||
|   } | ||||
|   getLibraryItemsInLibrary(libraryId) { | ||||
|     return this.libraryItems.filter(li => li.libraryId === libraryId) | ||||
|   } | ||||
| 
 | ||||
|   async updateLibraryItem(libraryItem) { | ||||
|     return this.updateLibraryItems([libraryItem]) | ||||
| @ -269,26 +266,6 @@ class Db { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async insertLibraryItem(libraryItem) { | ||||
|     return this.insertLibraryItems([libraryItem]) | ||||
|   } | ||||
| 
 | ||||
|   async insertLibraryItems(libraryItems) { | ||||
|     await Promise.all(libraryItems.map(async (li) => { | ||||
|       if (li && li.saveMetadata) return li.saveMetadata() | ||||
|       return null | ||||
|     })) | ||||
| 
 | ||||
|     return this.libraryItemsDb.insert(libraryItems).then((results) => { | ||||
|       Logger.debug(`[DB] Library Items inserted ${results.inserted}`) | ||||
|       this.libraryItems = this.libraryItems.concat(libraryItems) | ||||
|       return true | ||||
|     }).catch((error) => { | ||||
|       Logger.error(`[DB] Library Items insert failed ${error}`) | ||||
|       return false | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   removeLibraryItem(id) { | ||||
|     return this.libraryItemsDb.delete((record) => record.id === id).then((results) => { | ||||
|       Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`) | ||||
| @ -303,14 +280,6 @@ class Db { | ||||
|     return this.updateEntity('settings', this.serverSettings) | ||||
|   } | ||||
| 
 | ||||
|   getAllEntities(entityName) { | ||||
|     const entityDb = this.getEntityDb(entityName) | ||||
|     return entityDb.select(() => true).then((results) => results.data).catch((error) => { | ||||
|       Logger.error(`[DB] Failed to get all ${entityName}`, error) | ||||
|       return null | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   insertEntities(entityName, entities) { | ||||
|     var entityDb = this.getEntityDb(entityName) | ||||
|     return entityDb.insert(entities).then((results) => { | ||||
| @ -463,15 +432,6 @@ class Db { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   getAllSessions(selectFunc = () => true) { | ||||
|     return this.sessionsDb.select(selectFunc).then((results) => { | ||||
|       return results.data || [] | ||||
|     }).catch((error) => { | ||||
|       Logger.error('[Db] Failed to select sessions', error) | ||||
|       return [] | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   getPlaybackSession(id) { | ||||
|     return this.sessionsDb.select((pb) => pb.id == id).then((results) => { | ||||
|       if (results.data.length) { | ||||
| @ -484,15 +444,6 @@ class Db { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   selectUserSessions(userId) { | ||||
|     return this.sessionsDb.select((session) => session.userId === userId).then((results) => { | ||||
|       return results.data || [] | ||||
|     }).catch((error) => { | ||||
|       Logger.error(`[Db] Failed to select user sessions "${userId}"`, error) | ||||
|       return [] | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // Check if server was updated and previous version was earlier than param
 | ||||
|   checkPreviousVersionIsBefore(version) { | ||||
|     if (!this.previousVersion) return false | ||||
|  | ||||
							
								
								
									
										105
									
								
								server/Server.js
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								server/Server.js
									
									
									
									
									
								
							| @ -8,18 +8,18 @@ const rateLimit = require('./libs/expressRateLimit') | ||||
| const { version } = require('../package.json') | ||||
| 
 | ||||
| // Utils
 | ||||
| const dbMigration = require('./utils/dbMigration') | ||||
| const filePerms = require('./utils/filePerms') | ||||
| const fileUtils = require('./utils/fileUtils') | ||||
| const globals = require('./utils/globals') | ||||
| const Logger = require('./Logger') | ||||
| 
 | ||||
| const Auth = require('./Auth') | ||||
| const Watcher = require('./Watcher') | ||||
| const Scanner = require('./scanner/Scanner') | ||||
| const Db = require('./Db') | ||||
| const Database = require('./Database') | ||||
| const SocketAuthority = require('./SocketAuthority') | ||||
| 
 | ||||
| const routes = require('./routes/index') | ||||
| 
 | ||||
| const ApiRouter = require('./routers/ApiRouter') | ||||
| const HlsRouter = require('./routers/HlsRouter') | ||||
| 
 | ||||
| @ -29,7 +29,7 @@ const CoverManager = require('./managers/CoverManager') | ||||
| const AbMergeManager = require('./managers/AbMergeManager') | ||||
| const CacheManager = require('./managers/CacheManager') | ||||
| const LogManager = require('./managers/LogManager') | ||||
| const BackupManager = require('./managers/BackupManager') | ||||
| // const BackupManager = require('./managers/BackupManager') // TODO
 | ||||
| const PlaybackSessionManager = require('./managers/PlaybackSessionManager') | ||||
| const PodcastManager = require('./managers/PodcastManager') | ||||
| const AudioMetadataMangaer = require('./managers/AudioMetadataManager') | ||||
| @ -59,30 +59,30 @@ class Server { | ||||
|       filePerms.setDefaultDirSync(global.MetadataPath, false) | ||||
|     } | ||||
| 
 | ||||
|     this.db = new Db() | ||||
|     // this.db = new Db()
 | ||||
|     this.watcher = new Watcher() | ||||
|     this.auth = new Auth(this.db) | ||||
|     this.auth = new Auth() | ||||
| 
 | ||||
|     // Managers
 | ||||
|     this.taskManager = new TaskManager() | ||||
|     this.notificationManager = new NotificationManager(this.db) | ||||
|     this.emailManager = new EmailManager(this.db) | ||||
|     this.backupManager = new BackupManager(this.db) | ||||
|     this.logManager = new LogManager(this.db) | ||||
|     this.notificationManager = new NotificationManager() | ||||
|     this.emailManager = new EmailManager() | ||||
|     // this.backupManager = new BackupManager(this.db)
 | ||||
|     this.logManager = new LogManager() | ||||
|     this.cacheManager = new CacheManager() | ||||
|     this.abMergeManager = new AbMergeManager(this.db, this.taskManager) | ||||
|     this.playbackSessionManager = new PlaybackSessionManager(this.db) | ||||
|     this.coverManager = new CoverManager(this.db, this.cacheManager) | ||||
|     this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager) | ||||
|     this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager) | ||||
|     this.rssFeedManager = new RssFeedManager(this.db) | ||||
|     this.abMergeManager = new AbMergeManager(this.taskManager) | ||||
|     this.playbackSessionManager = new PlaybackSessionManager() | ||||
|     this.coverManager = new CoverManager(this.cacheManager) | ||||
|     this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager) | ||||
|     this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager) | ||||
|     this.rssFeedManager = new RssFeedManager() | ||||
| 
 | ||||
|     this.scanner = new Scanner(this.db, this.coverManager, this.taskManager) | ||||
|     this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager) | ||||
|     this.scanner = new Scanner(this.coverManager, this.taskManager) | ||||
|     this.cronManager = new CronManager(this.scanner, this.podcastManager) | ||||
| 
 | ||||
|     // Routers
 | ||||
|     this.apiRouter = new ApiRouter(this) | ||||
|     this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager) | ||||
|     this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager) | ||||
| 
 | ||||
|     Logger.logManager = this.logManager | ||||
| 
 | ||||
| @ -98,38 +98,28 @@ class Server { | ||||
|     Logger.info('[Server] Init v' + version) | ||||
|     await this.playbackSessionManager.removeOrphanStreams() | ||||
| 
 | ||||
|     const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
 | ||||
|     if (previousVersion) { | ||||
|       Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`) | ||||
|     } | ||||
|     if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
 | ||||
|       Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`) | ||||
|       await dbMigration.migrate(this.db) | ||||
|     } else { | ||||
|       await this.db.init() | ||||
|     } | ||||
|     await Database.init(false) | ||||
| 
 | ||||
|     // Create token secret if does not exist (Added v2.1.0)
 | ||||
|     if (!this.db.serverSettings.tokenSecret) { | ||||
|     if (!Database.serverSettings.tokenSecret) { | ||||
|       await this.auth.initTokenSecret() | ||||
|     } | ||||
| 
 | ||||
|     await this.cleanUserData() // Remove invalid user item progress
 | ||||
|     await this.purgeMetadata() // Remove metadata folders without library item
 | ||||
|     await this.playbackSessionManager.removeInvalidSessions() | ||||
|     await this.cacheManager.ensureCachePaths() | ||||
| 
 | ||||
|     await this.backupManager.init() | ||||
|     // await this.backupManager.init() // TODO: Implement backups
 | ||||
|     await this.logManager.init() | ||||
|     await this.apiRouter.checkRemoveEmptySeries(this.db.series) // Remove empty series
 | ||||
|     await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
 | ||||
|     await this.rssFeedManager.init() | ||||
|     this.cronManager.init() | ||||
| 
 | ||||
|     if (this.db.serverSettings.scannerDisableWatcher) { | ||||
|     if (Database.serverSettings.scannerDisableWatcher) { | ||||
|       Logger.info(`[Server] Watcher is disabled`) | ||||
|       this.watcher.disabled = true | ||||
|     } else { | ||||
|       this.watcher.initWatcher(this.db.libraries) | ||||
|       this.watcher.initWatcher(Database.libraries) | ||||
|       this.watcher.on('files', this.filesChanged.bind(this)) | ||||
|     } | ||||
|   } | ||||
| @ -162,6 +152,7 @@ class Server { | ||||
|     // Static folder
 | ||||
|     router.use(express.static(Path.join(global.appRoot, 'static'))) | ||||
| 
 | ||||
|     // router.use('/api/v1', routes) // TODO: New routes
 | ||||
|     router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router) | ||||
|     router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) | ||||
| 
 | ||||
| @ -203,7 +194,7 @@ class Server { | ||||
|     router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) | ||||
|     router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) | ||||
|     router.post('/init', (req, res) => { | ||||
|       if (this.db.hasRootUser) { | ||||
|       if (Database.hasRootUser) { | ||||
|         Logger.error(`[Server] attempt to init server when server already has a root user`) | ||||
|         return res.sendStatus(500) | ||||
|       } | ||||
| @ -213,8 +204,8 @@ class Server { | ||||
|       // status check for client to see if server has been initialized
 | ||||
|       // server has been initialized if a root user exists
 | ||||
|       const payload = { | ||||
|         isInit: this.db.hasRootUser, | ||||
|         language: this.db.serverSettings.language | ||||
|         isInit: Database.hasRootUser, | ||||
|         language: Database.serverSettings.language | ||||
|       } | ||||
|       if (!payload.isInit) { | ||||
|         payload.ConfigPath = global.ConfigPath | ||||
| @ -243,7 +234,7 @@ class Server { | ||||
|     let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : '' | ||||
|     if (!rootPash) Logger.warn(`[Server] Creating root user with no password`) | ||||
|     let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username }) | ||||
|     await this.db.createRootUser(newRoot.username, rootPash, rootToken) | ||||
|     await Database.createRootUser(newRoot.username, rootPash, rootToken) | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| @ -261,7 +252,7 @@ class Server { | ||||
| 
 | ||||
|     let purged = 0 | ||||
|     await Promise.all(foldersInItemsMetadata.map(async foldername => { | ||||
|       const hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername) | ||||
|       const hasMatchingItem = Database.libraryItems.find(ab => ab.id === foldername) | ||||
|       if (!hasMatchingItem) { | ||||
|         const folderPath = Path.join(itemsMetadata, foldername) | ||||
|         Logger.debug(`[Server] Purging unused metadata ${folderPath}`) | ||||
| @ -281,26 +272,26 @@ class Server { | ||||
| 
 | ||||
|   // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
 | ||||
|   async cleanUserData() { | ||||
|     for (let i = 0; i < this.db.users.length; i++) { | ||||
|       const _user = this.db.users[i] | ||||
|       let hasUpdated = false | ||||
|     for (const _user of Database.users) { | ||||
|       if (_user.mediaProgress.length) { | ||||
|         const lengthBefore = _user.mediaProgress.length | ||||
|         _user.mediaProgress = _user.mediaProgress.filter(mp => { | ||||
|           const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) | ||||
|           if (!libraryItem) return false | ||||
|           if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
 | ||||
|           return true | ||||
|         }) | ||||
|         for (const mediaProgress of _user.mediaProgress) { | ||||
|           const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId) | ||||
|           if (libraryItem && mediaProgress.episodeId) { | ||||
|             const episode = libraryItem.media.checkHasEpisode?.(mediaProgress.episodeId) | ||||
|             if (episode) continue | ||||
|           } else { | ||||
|             continue | ||||
|           } | ||||
| 
 | ||||
|         if (lengthBefore > _user.mediaProgress.length) { | ||||
|           Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`) | ||||
|           hasUpdated = true | ||||
|           Logger.debug(`[Server] Removing media progress ${mediaProgress.id} data from user ${_user.username}`) | ||||
|           await Database.removeMediaProgress(mediaProgress.id) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       let hasUpdated = false | ||||
|       if (_user.seriesHideFromContinueListening.length) { | ||||
|         _user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => { | ||||
|           if (!this.db.series.some(se => se.id === seriesId)) { // Series removed
 | ||||
|           if (!Database.series.some(se => se.id === seriesId)) { // Series removed
 | ||||
|             hasUpdated = true | ||||
|             return false | ||||
|           } | ||||
| @ -308,7 +299,7 @@ class Server { | ||||
|         }) | ||||
|       } | ||||
|       if (hasUpdated) { | ||||
|         await this.db.updateEntity('user', _user) | ||||
|         await Database.updateUser(_user) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -321,8 +312,8 @@ class Server { | ||||
| 
 | ||||
|   getLoginRateLimiter() { | ||||
|     return rateLimit({ | ||||
|       windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes
 | ||||
|       max: this.db.serverSettings.rateLimitLoginRequests, | ||||
|       windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
 | ||||
|       max: Database.serverSettings.rateLimitLoginRequests, | ||||
|       skipSuccessfulRequests: true, | ||||
|       onLimitReached: this.loginLimitReached | ||||
|     }) | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const SocketIO = require('socket.io') | ||||
| const Logger = require('./Logger') | ||||
| const Database = require('./Database') | ||||
| 
 | ||||
| class SocketAuthority { | ||||
|   constructor() { | ||||
| @ -18,7 +19,7 @@ class SocketAuthority { | ||||
|         onlineUsersMap[client.user.id].connections++ | ||||
|       } else { | ||||
|         onlineUsersMap[client.user.id] = { | ||||
|           ...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems), | ||||
|           ...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems), | ||||
|           connections: 1 | ||||
|         } | ||||
|       } | ||||
| @ -107,7 +108,7 @@ class SocketAuthority { | ||||
|           delete this.clients[socket.id] | ||||
|         } else { | ||||
|           Logger.debug('[Server] User Offline ' + _client.user.username) | ||||
|           this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems)) | ||||
|           this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems)) | ||||
| 
 | ||||
|           const disconnectTime = Date.now() - _client.connected_at | ||||
|           Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`) | ||||
| @ -160,11 +161,11 @@ class SocketAuthority { | ||||
| 
 | ||||
|     Logger.debug(`[Server] User Online ${client.user.username}`) | ||||
| 
 | ||||
|     this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems)) | ||||
|     this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems)) | ||||
| 
 | ||||
|     // Update user lastSeen
 | ||||
|     user.lastSeen = Date.now() | ||||
|     await this.Server.db.updateEntity('user', user) | ||||
|     await Database.updateUser(user) | ||||
| 
 | ||||
|     const initialPayload = { | ||||
|       userId: client.user.id, | ||||
| @ -186,7 +187,7 @@ class SocketAuthority { | ||||
| 
 | ||||
|       if (client.user) { | ||||
|         Logger.debug('[Server] User Offline ' + client.user.username) | ||||
|         this.adminEmitter('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems)) | ||||
|         this.adminEmitter('user_offline', client.user.toJSONForPublic(null, Database.libraryItems)) | ||||
|       } | ||||
| 
 | ||||
|       delete this.clients[socketId].user | ||||
|  | ||||
| @ -4,6 +4,7 @@ const { createNewSortInstance } = require('../libs/fastSort') | ||||
| 
 | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const { reqSupportsWebp } = require('../utils/index') | ||||
| 
 | ||||
| @ -21,7 +22,7 @@ class AuthorController { | ||||
| 
 | ||||
|     // Used on author landing page to include library items and items grouped in series
 | ||||
|     if (include.includes('items')) { | ||||
|       authorJson.libraryItems = this.db.libraryItems.filter(li => { | ||||
|       authorJson.libraryItems = Database.libraryItems.filter(li => { | ||||
|         if (libraryId && li.libraryId !== libraryId) return false | ||||
|         if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
 | ||||
|         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) | ||||
| @ -97,23 +98,29 @@ class AuthorController { | ||||
|     const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name | ||||
| 
 | ||||
|     // Check if author name matches another author and merge the authors
 | ||||
|     const existingAuthor = authorNameUpdate ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false | ||||
|     const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false | ||||
|     if (existingAuthor) { | ||||
|       const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id)) | ||||
|       const bookAuthorsToCreate = [] | ||||
|       const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id)) | ||||
|       itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
 | ||||
|         libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor) | ||||
|         bookAuthorsToCreate.push({ | ||||
|           bookId: libraryItem.media.id, | ||||
|           authorId: existingAuthor.id | ||||
|         }) | ||||
|       }) | ||||
|       if (itemsWithAuthor.length) { | ||||
|         await this.db.updateLibraryItems(itemsWithAuthor) | ||||
|         await Database.removeBulkBookAuthors(req.author.id) // Remove all old BookAuthor
 | ||||
|         await Database.createBulkBookAuthors(bookAuthorsToCreate) // Create all new BookAuthor
 | ||||
|         SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded())) | ||||
|       } | ||||
| 
 | ||||
|       // Remove old author
 | ||||
|       await this.db.removeEntity('author', req.author.id) | ||||
|       await Database.removeAuthor(req.author.id) | ||||
|       SocketAuthority.emitter('author_removed', req.author.toJSON()) | ||||
| 
 | ||||
|       // Send updated num books for merged author
 | ||||
|       const numBooks = this.db.libraryItems.filter(li => { | ||||
|       const numBooks = Database.libraryItems.filter(li => { | ||||
|         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id) | ||||
|       }).length | ||||
|       SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) | ||||
| @ -131,18 +138,17 @@ class AuthorController { | ||||
|         req.author.updatedAt = Date.now() | ||||
| 
 | ||||
|         if (authorNameUpdate) { // Update author name on all books
 | ||||
|           const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id)) | ||||
|           const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id)) | ||||
|           itemsWithAuthor.forEach(libraryItem => { | ||||
|             libraryItem.media.metadata.updateAuthor(req.author) | ||||
|           }) | ||||
|           if (itemsWithAuthor.length) { | ||||
|             await this.db.updateLibraryItems(itemsWithAuthor) | ||||
|             SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded())) | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         await this.db.updateEntity('author', req.author) | ||||
|         const numBooks = this.db.libraryItems.filter(li => { | ||||
|         await Database.updateAuthor(req.author) | ||||
|         const numBooks = Database.libraryItems.filter(li => { | ||||
|           return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) | ||||
|         }).length | ||||
|         SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||
| @ -159,7 +165,7 @@ class AuthorController { | ||||
|     var q = (req.query.q || '').toLowerCase() | ||||
|     if (!q) return res.json([]) | ||||
|     var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25 | ||||
|     var authors = this.db.authors.filter(au => au.name.toLowerCase().includes(q)) | ||||
|     var authors = Database.authors.filter(au => au.name.toLowerCase().includes(q)) | ||||
|     authors = authors.slice(0, limit) | ||||
|     res.json({ | ||||
|       results: authors | ||||
| @ -204,8 +210,8 @@ class AuthorController { | ||||
|     if (hasUpdates) { | ||||
|       req.author.updatedAt = Date.now() | ||||
| 
 | ||||
|       await this.db.updateEntity('author', req.author) | ||||
|       const numBooks = this.db.libraryItems.filter(li => { | ||||
|       await Database.updateAuthor(req.author) | ||||
|       const numBooks = Database.libraryItems.filter(li => { | ||||
|         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) | ||||
|       }).length | ||||
|       SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||
| @ -238,7 +244,7 @@ class AuthorController { | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     var author = this.db.authors.find(au => au.id === req.params.id) | ||||
|     var author = Database.authors.find(au => au.id === req.params.id) | ||||
|     if (!author) return res.sendStatus(404) | ||||
| 
 | ||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const Collection = require('../objects/Collection') | ||||
| 
 | ||||
| @ -13,22 +14,22 @@ class CollectionController { | ||||
|     if (!success) { | ||||
|       return res.status(500).send('Invalid collection data') | ||||
|     } | ||||
|     var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.insertEntity('collection', newCollection) | ||||
|     var jsonExpanded = newCollection.toJSONExpanded(Database.libraryItems) | ||||
|     await Database.createCollection(newCollection) | ||||
|     SocketAuthority.emitter('collection_added', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
|   } | ||||
| 
 | ||||
|   findAll(req, res) { | ||||
|     res.json({ | ||||
|       collections: this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems)) | ||||
|       collections: Database.collections.map(c => c.toJSONExpanded(Database.libraryItems)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   findOne(req, res) { | ||||
|     const includeEntities = (req.query.include || '').split(',') | ||||
| 
 | ||||
|     const collectionExpanded = req.collection.toJSONExpanded(this.db.libraryItems) | ||||
|     const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|     if (includeEntities.includes('rssfeed')) { | ||||
|       const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id) | ||||
| @ -41,9 +42,9 @@ class CollectionController { | ||||
|   async update(req, res) { | ||||
|     const collection = req.collection | ||||
|     const wasUpdated = collection.update(req.body) | ||||
|     const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       await Database.updateCollection(collection) | ||||
|       SocketAuthority.emitter('collection_updated', jsonExpanded) | ||||
|     } | ||||
|     res.json(jsonExpanded) | ||||
| @ -51,19 +52,19 @@ class CollectionController { | ||||
| 
 | ||||
|   async delete(req, res) { | ||||
|     const collection = req.collection | ||||
|     const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|     // Close rss feed - remove from db and emit socket event
 | ||||
|     await this.rssFeedManager.closeFeedForEntityId(collection.id) | ||||
| 
 | ||||
|     await this.db.removeEntity('collection', collection.id) | ||||
|     await Database.removeCollection(collection.id) | ||||
|     SocketAuthority.emitter('collection_removed', jsonExpanded) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async addBook(req, res) { | ||||
|     const collection = req.collection | ||||
|     const libraryItem = this.db.libraryItems.find(li => li.id === req.body.id) | ||||
|     const libraryItem = Database.libraryItems.find(li => li.id === req.body.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(500).send('Book not found') | ||||
|     } | ||||
| @ -74,8 +75,14 @@ class CollectionController { | ||||
|       return res.status(500).send('Book already in collection') | ||||
|     } | ||||
|     collection.addBook(req.body.id) | ||||
|     const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.updateEntity('collection', collection) | ||||
|     const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|     const collectionBook = { | ||||
|       collectionId: collection.id, | ||||
|       bookId: libraryItem.media.id, | ||||
|       order: collection.books.length | ||||
|     } | ||||
|     await Database.createCollectionBook(collectionBook) | ||||
|     SocketAuthority.emitter('collection_updated', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
|   } | ||||
| @ -83,13 +90,18 @@ class CollectionController { | ||||
|   // DELETE: api/collections/:id/book/:bookId
 | ||||
|   async removeBook(req, res) { | ||||
|     const collection = req.collection | ||||
|     const libraryItem = Database.libraryItems.find(li => li.id === req.params.bookId) | ||||
|     if (!libraryItem) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     if (collection.books.includes(req.params.bookId)) { | ||||
|       collection.removeBook(req.params.bookId) | ||||
|       var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
|       SocketAuthority.emitter('collection_updated', jsonExpanded) | ||||
|       await Database.updateCollection(collection) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(collection.toJSONExpanded(Database.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/add
 | ||||
| @ -98,19 +110,30 @@ class CollectionController { | ||||
|     if (!req.body.books || !req.body.books.length) { | ||||
|       return res.status(500).send('Invalid request body') | ||||
|     } | ||||
|     var bookIdsToAdd = req.body.books | ||||
|     var hasUpdated = false | ||||
|     for (let i = 0; i < bookIdsToAdd.length; i++) { | ||||
|       if (!collection.books.includes(bookIdsToAdd[i])) { | ||||
|         collection.addBook(bookIdsToAdd[i]) | ||||
|     const bookIdsToAdd = req.body.books | ||||
|     const collectionBooksToAdd = [] | ||||
|     let hasUpdated = false | ||||
| 
 | ||||
|     let order = collection.books.length | ||||
|     for (const libraryItemId of bookIdsToAdd) { | ||||
|       const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId) | ||||
|       if (!libraryItem) continue | ||||
|       if (!collection.books.includes(libraryItemId)) { | ||||
|         collection.addBook(libraryItemId) | ||||
|         collectionBooksToAdd.push({ | ||||
|           collectionId: collection.id, | ||||
|           bookId: libraryItem.media.id, | ||||
|           order: order++ | ||||
|         }) | ||||
|         hasUpdated = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) | ||||
|       await Database.createBulkCollectionBooks(collectionBooksToAdd) | ||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems)) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(collection.toJSONExpanded(Database.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/collections/:id/batch/remove
 | ||||
| @ -120,23 +143,26 @@ class CollectionController { | ||||
|       return res.status(500).send('Invalid request body') | ||||
|     } | ||||
|     var bookIdsToRemove = req.body.books | ||||
|     var hasUpdated = false | ||||
|     for (let i = 0; i < bookIdsToRemove.length; i++) { | ||||
|       if (collection.books.includes(bookIdsToRemove[i])) { | ||||
|         collection.removeBook(bookIdsToRemove[i]) | ||||
|     let hasUpdated = false | ||||
|     for (const libraryItemId of bookIdsToRemove) { | ||||
|       const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId) | ||||
|       if (!libraryItem) continue | ||||
| 
 | ||||
|       if (collection.books.includes(libraryItemId)) { | ||||
|         collection.removeBook(libraryItemId) | ||||
|         hasUpdated = true | ||||
|       } | ||||
|     } | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('collection', collection) | ||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) | ||||
|       await Database.updateCollection(collection) | ||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems)) | ||||
|     } | ||||
|     res.json(collection.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(collection.toJSONExpanded(Database.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     if (req.params.id) { | ||||
|       const collection = this.db.collections.find(c => c.id === req.params.id) | ||||
|       const collection = Database.collections.find(c => c.id === req.params.id) | ||||
|       if (!collection) { | ||||
|         return res.status(404).send('Collection not found') | ||||
|       } | ||||
|  | ||||
| @ -1,22 +1,23 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class EmailController { | ||||
|   constructor() { } | ||||
| 
 | ||||
|   getSettings(req, res) { | ||||
|     res.json({ | ||||
|       settings: this.db.emailSettings | ||||
|       settings: Database.emailSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async updateSettings(req, res) { | ||||
|     const updated = this.db.emailSettings.update(req.body) | ||||
|     const updated = Database.emailSettings.update(req.body) | ||||
|     if (updated) { | ||||
|       await this.db.updateEntity('settings', this.db.emailSettings) | ||||
|       await Database.updateSetting(Database.emailSettings) | ||||
|     } | ||||
|     res.json({ | ||||
|       settings: this.db.emailSettings | ||||
|       settings: Database.emailSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -36,24 +37,24 @@ class EmailController { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const updated = this.db.emailSettings.update({ | ||||
|     const updated = Database.emailSettings.update({ | ||||
|       ereaderDevices | ||||
|     }) | ||||
|     if (updated) { | ||||
|       await this.db.updateEntity('settings', this.db.emailSettings) | ||||
|       await Database.updateSetting(Database.emailSettings) | ||||
|       SocketAuthority.adminEmitter('ereader-devices-updated', { | ||||
|         ereaderDevices: this.db.emailSettings.ereaderDevices | ||||
|         ereaderDevices: Database.emailSettings.ereaderDevices | ||||
|       }) | ||||
|     } | ||||
|     res.json({ | ||||
|       ereaderDevices: this.db.emailSettings.ereaderDevices | ||||
|       ereaderDevices: Database.emailSettings.ereaderDevices | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async sendEBookToDevice(req, res) { | ||||
|     Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) | ||||
| 
 | ||||
|     const libraryItem = this.db.getLibraryItem(req.body.libraryItemId) | ||||
|     const libraryItem = Database.getLibraryItem(req.body.libraryItemId) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(404).send('Library item not found') | ||||
|     } | ||||
| @ -67,7 +68,7 @@ class EmailController { | ||||
|       return res.status(404).send('EBook file not found') | ||||
|     } | ||||
| 
 | ||||
|     const device = this.db.emailSettings.getEReaderDevice(req.body.deviceName) | ||||
|     const device = Database.emailSettings.getEReaderDevice(req.body.deviceName) | ||||
|     if (!device) { | ||||
|       return res.status(404).send('E-reader device not found') | ||||
|     } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Path = require('path') | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const fs = require('../libs/fsExtra') | ||||
| 
 | ||||
| class FileSystemController { | ||||
| @ -16,7 +17,7 @@ class FileSystemController { | ||||
|     }) | ||||
| 
 | ||||
|     // Do not include existing mapped library paths in response
 | ||||
|     this.db.libraries.forEach(lib => { | ||||
|     Database.libraries.forEach(lib => { | ||||
|       lib.folders.forEach((folder) => { | ||||
|         let dir = folder.fullPath | ||||
|         if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') | ||||
|  | ||||
| @ -9,6 +9,9 @@ const { sort, createNewSortInstance } = require('../libs/fastSort') | ||||
| const naturalSort = createNewSortInstance({ | ||||
|   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare | ||||
| }) | ||||
| 
 | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class LibraryController { | ||||
|   constructor() { } | ||||
| 
 | ||||
| @ -40,13 +43,13 @@ class LibraryController { | ||||
|     } | ||||
| 
 | ||||
|     const library = new Library() | ||||
|     newLibraryPayload.displayOrder = this.db.libraries.length + 1 | ||||
|     newLibraryPayload.displayOrder = Database.libraries.length + 1 | ||||
|     library.setData(newLibraryPayload) | ||||
|     await this.db.insertEntity('library', library) | ||||
|     await Database.createLibrary(library) | ||||
| 
 | ||||
|     // Only emit to users with access to library
 | ||||
|     const userFilter = (user) => { | ||||
|       return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id) | ||||
|       return user.checkCanAccessLibrary?.(library.id) | ||||
|     } | ||||
|     SocketAuthority.emitter('library_added', library.toJSON(), userFilter) | ||||
| 
 | ||||
| @ -58,14 +61,15 @@ class LibraryController { | ||||
| 
 | ||||
|   findAll(req, res) { | ||||
|     const librariesAccessible = req.user.librariesAccessible || [] | ||||
|     if (librariesAccessible && librariesAccessible.length) { | ||||
|     if (librariesAccessible.length) { | ||||
|       return res.json({ | ||||
|         libraries: this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()) | ||||
|         libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()) | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       libraries: this.db.libraries.map(lib => lib.toJSON()) | ||||
|       libraries: Database.libraries.map(lib => lib.toJSON()) | ||||
|       // libraries: Database.libraries.map(lib => lib.toJSON())
 | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -75,7 +79,7 @@ class LibraryController { | ||||
|       return res.json({ | ||||
|         filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems), | ||||
|         issues: req.libraryItems.filter(li => li.hasIssues).length, | ||||
|         numUserPlaylists: this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length, | ||||
|         numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length, | ||||
|         library: req.library | ||||
|       }) | ||||
|     } | ||||
| @ -128,14 +132,14 @@ class LibraryController { | ||||
|       this.cronManager.updateLibraryScanCron(library) | ||||
| 
 | ||||
|       // Remove libraryItems no longer in library
 | ||||
|       const itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path)) | ||||
|       const itemsToRemove = Database.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path)) | ||||
|       if (itemsToRemove.length) { | ||||
|         Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`) | ||||
|         for (let i = 0; i < itemsToRemove.length; i++) { | ||||
|           await this.handleDeleteLibraryItem(itemsToRemove[i]) | ||||
|         } | ||||
|       } | ||||
|       await this.db.updateEntity('library', library) | ||||
|       await Database.updateLibrary(library) | ||||
| 
 | ||||
|       // Only emit to users with access to library
 | ||||
|       const userFilter = (user) => { | ||||
| @ -153,21 +157,21 @@ class LibraryController { | ||||
|     this.watcher.removeLibrary(library) | ||||
| 
 | ||||
|     // Remove collections for library
 | ||||
|     const collections = this.db.collections.filter(c => c.libraryId === library.id) | ||||
|     const collections = Database.collections.filter(c => c.libraryId === library.id) | ||||
|     for (const collection of collections) { | ||||
|       Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`) | ||||
|       await this.db.removeEntity('collection', collection.id) | ||||
|       await Database.removeCollection(collection.id) | ||||
|     } | ||||
| 
 | ||||
|     // Remove items in this library
 | ||||
|     const libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id) | ||||
|     const libraryItems = Database.libraryItems.filter(li => li.libraryId === library.id) | ||||
|     Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`) | ||||
|     for (let i = 0; i < libraryItems.length; i++) { | ||||
|       await this.handleDeleteLibraryItem(libraryItems[i]) | ||||
|     } | ||||
| 
 | ||||
|     const libraryJson = library.toJSON() | ||||
|     await this.db.removeEntity('library', library.id) | ||||
|     await Database.removeLibrary(library.id) | ||||
|     SocketAuthority.emitter('library_removed', libraryJson) | ||||
|     return res.json(libraryJson) | ||||
|   } | ||||
| @ -209,7 +213,7 @@ class LibraryController { | ||||
|     // If also filtering by series, will not collapse the filtered series as this would lead
 | ||||
|     // to series having a collapsed series that is just that series.
 | ||||
|     if (payload.collapseseries) { | ||||
|       let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries, req.library.settings.hideSingleBookSeries) | ||||
|       let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, Database.series, filterSeries, req.library.settings.hideSingleBookSeries) | ||||
| 
 | ||||
|       if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { | ||||
|         libraryItems = collapsedItems | ||||
| @ -237,7 +241,7 @@ class LibraryController { | ||||
|       // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
 | ||||
|       sortArray.push({ | ||||
|         asc: (li) => { | ||||
|           if (this.db.serverSettings.sortingIgnorePrefix) { | ||||
|           if (Database.serverSettings.sortingIgnorePrefix) { | ||||
|             return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix | ||||
|           } else { | ||||
|             return li.collapsedSeries?.name || li.media.metadata.title | ||||
| @ -255,7 +259,7 @@ class LibraryController { | ||||
| 
 | ||||
|       // Handle server setting sortingIgnorePrefix
 | ||||
|       const sortByTitle = sortKey === 'media.metadata.title' | ||||
|       if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) { | ||||
|       if (sortByTitle && Database.serverSettings.sortingIgnorePrefix) { | ||||
|         // BookMetadata.js has titleIgnorePrefix getter
 | ||||
|         sortKey += 'IgnorePrefix' | ||||
|       } | ||||
| @ -267,7 +271,7 @@ class LibraryController { | ||||
|         sortArray.push({ | ||||
|           asc: (li) => { | ||||
|             if (li.collapsedSeries) { | ||||
|               return this.db.serverSettings.sortingIgnorePrefix ? | ||||
|               return Database.serverSettings.sortingIgnorePrefix ? | ||||
|                 li.collapsedSeries.nameIgnorePrefix : | ||||
|                 li.collapsedSeries.name | ||||
|             } else { | ||||
| @ -284,7 +288,7 @@ class LibraryController { | ||||
|           if (mediaIsBook && sortBySequence) { | ||||
|             return li.media.metadata.getSeries(filterSeries).sequence | ||||
|           } else if (mediaIsBook && sortByTitle && li.collapsedSeries) { | ||||
|             return this.db.serverSettings.sortingIgnorePrefix ? | ||||
|             return Database.serverSettings.sortingIgnorePrefix ? | ||||
|               li.collapsedSeries.nameIgnorePrefix : | ||||
|               li.collapsedSeries.name | ||||
|           } else { | ||||
| @ -405,7 +409,7 @@ class LibraryController { | ||||
|       include: include.join(',') | ||||
|     } | ||||
| 
 | ||||
|     let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries) | ||||
|     let series = libraryHelpers.getSeriesFromBooks(libraryItems, Database.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries) | ||||
| 
 | ||||
|     const direction = payload.sortDesc ? 'desc' : 'asc' | ||||
|     series = naturalSort(series).by([ | ||||
| @ -422,7 +426,7 @@ class LibraryController { | ||||
|           } else if (payload.sortBy === 'lastBookAdded') { | ||||
|             return Math.max(...(se.books).map(x => x.addedAt), 0) | ||||
|           } else { // sort by name
 | ||||
|             return this.db.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name | ||||
|             return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| @ -466,7 +470,7 @@ class LibraryController { | ||||
|       include: include.join(',') | ||||
|     } | ||||
| 
 | ||||
|     let collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => { | ||||
|     let collections = Database.collections.filter(c => c.libraryId === req.library.id).map(c => { | ||||
|       const expanded = c.toJSONExpanded(libraryItems, payload.minified) | ||||
| 
 | ||||
|       // If all books restricted to user in this collection then hide this collection
 | ||||
| @ -493,7 +497,7 @@ class LibraryController { | ||||
| 
 | ||||
|   // api/libraries/:id/playlists
 | ||||
|   async getUserPlaylistsForLibrary(req, res) { | ||||
|     let playlistsForUser = this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(this.db.libraryItems)) | ||||
|     let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems)) | ||||
| 
 | ||||
|     const payload = { | ||||
|       results: [], | ||||
| @ -517,7 +521,7 @@ class LibraryController { | ||||
|       return res.status(400).send('Invalid library media type') | ||||
|     } | ||||
| 
 | ||||
|     let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id) | ||||
|     let libraryItems = Database.libraryItems.filter(li => li.libraryId === req.library.id) | ||||
|     let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems) | ||||
|     albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
 | ||||
| 
 | ||||
| @ -561,26 +565,26 @@ class LibraryController { | ||||
|     var orderdata = req.body | ||||
|     var hasUpdates = false | ||||
|     for (let i = 0; i < orderdata.length; i++) { | ||||
|       var library = this.db.libraries.find(lib => lib.id === orderdata[i].id) | ||||
|       var library = Database.libraries.find(lib => lib.id === orderdata[i].id) | ||||
|       if (!library) { | ||||
|         Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) | ||||
|         return res.sendStatus(500) | ||||
|       } | ||||
|       if (library.update({ displayOrder: orderdata[i].newOrder })) { | ||||
|         hasUpdates = true | ||||
|         await this.db.updateEntity('library', library) | ||||
|         await Database.updateLibrary(library) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (hasUpdates) { | ||||
|       this.db.libraries.sort((a, b) => a.displayOrder - b.displayOrder) | ||||
|       Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder) | ||||
|       Logger.debug(`[LibraryController] Updated library display orders`) | ||||
|     } else { | ||||
|       Logger.debug(`[LibraryController] Library orders were up to date`) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       libraries: this.db.libraries.map(lib => lib.toJSON()) | ||||
|       libraries: Database.libraries.map(lib => lib.toJSON()) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -610,7 +614,7 @@ class LibraryController { | ||||
|       if (queryResult.series?.length) { | ||||
|         queryResult.series.forEach((se) => { | ||||
|           if (!seriesMatches[se.id]) { | ||||
|             const _series = this.db.series.find(_se => _se.id === se.id) | ||||
|             const _series = Database.series.find(_se => _se.id === se.id) | ||||
|             if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] } | ||||
|           } else { | ||||
|             seriesMatches[se.id].books.push(li.toJSON()) | ||||
| @ -620,7 +624,7 @@ class LibraryController { | ||||
|       if (queryResult.authors?.length) { | ||||
|         queryResult.authors.forEach((au) => { | ||||
|           if (!authorMatches[au.id]) { | ||||
|             const _author = this.db.authors.find(_au => _au.id === au.id) | ||||
|             const _author = Database.authors.find(_au => _au.id === au.id) | ||||
|             if (_author) { | ||||
|               authorMatches[au.id] = _author.toJSON() | ||||
|               authorMatches[au.id].numBooks = 1 | ||||
| @ -687,7 +691,7 @@ class LibraryController { | ||||
|       if (li.media.metadata.authors && li.media.metadata.authors.length) { | ||||
|         li.media.metadata.authors.forEach((au) => { | ||||
|           if (!authors[au.id]) { | ||||
|             const _author = this.db.authors.find(_au => _au.id === au.id) | ||||
|             const _author = Database.authors.find(_au => _au.id === au.id) | ||||
|             if (_author) { | ||||
|               authors[au.id] = _author.toJSON() | ||||
|               authors[au.id].numBooks = 1 | ||||
| @ -749,7 +753,7 @@ class LibraryController { | ||||
|     } | ||||
| 
 | ||||
|     if (itemsUpdated.length) { | ||||
|       await this.db.updateLibraryItems(itemsUpdated) | ||||
|       await Database.updateBulkBooks(itemsUpdated.map(i => i.media)) | ||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||
|     } | ||||
| 
 | ||||
| @ -774,7 +778,7 @@ class LibraryController { | ||||
|     } | ||||
| 
 | ||||
|     if (itemsUpdated.length) { | ||||
|       await this.db.updateLibraryItems(itemsUpdated) | ||||
|       await Database.updateBulkBooks(itemsUpdated.map(i => i.media)) | ||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||
|     } | ||||
| 
 | ||||
| @ -858,12 +862,12 @@ class LibraryController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     const library = this.db.libraries.find(lib => lib.id === req.params.id) | ||||
|     const library = Database.libraries.find(lib => lib.id === req.params.id) | ||||
|     if (!library) { | ||||
|       return res.status(404).send('Library not found') | ||||
|     } | ||||
|     req.library = library | ||||
|     req.libraryItems = this.db.libraryItems.filter(li => { | ||||
|     req.libraryItems = Database.libraryItems.filter(li => { | ||||
|       return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li) | ||||
|     }) | ||||
|     next() | ||||
|  | ||||
| @ -2,9 +2,10 @@ const Path = require('path') | ||||
| const fs = require('../libs/fsExtra') | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const zipHelpers = require('../utils/zipHelpers') | ||||
| const { reqSupportsWebp, isNullOrNaN } = require('../utils/index') | ||||
| const { reqSupportsWebp } = require('../utils/index') | ||||
| const { ScanResult } = require('../utils/constants') | ||||
| const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') | ||||
| 
 | ||||
| @ -31,7 +32,7 @@ class LibraryItemController { | ||||
|       if (item.mediaType == 'book') { | ||||
|         if (includeEntities.includes('authors')) { | ||||
|           item.media.metadata.authors = item.media.metadata.authors.map(au => { | ||||
|             var author = this.db.authors.find(_au => _au.id === au.id) | ||||
|             var author = Database.authors.find(_au => _au.id === au.id) | ||||
|             if (!author) return null | ||||
|             return { | ||||
|               ...author | ||||
| @ -61,7 +62,7 @@ class LibraryItemController { | ||||
|     const hasUpdates = libraryItem.update(req.body) | ||||
|     if (hasUpdates) { | ||||
|       Logger.debug(`[LibraryItemController] Updated now saving`) | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
|     res.json(libraryItem.toJSON()) | ||||
| @ -139,7 +140,7 @@ class LibraryItemController { | ||||
|       } | ||||
| 
 | ||||
|       Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
|     res.json({ | ||||
| @ -174,7 +175,7 @@ class LibraryItemController { | ||||
|       return res.status(500).send('Unknown error occurred') | ||||
|     } | ||||
| 
 | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json({ | ||||
|       success: true, | ||||
| @ -194,7 +195,7 @@ class LibraryItemController { | ||||
|       return res.status(500).send(validationResult.error) | ||||
|     } | ||||
|     if (validationResult.updated) { | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
|     res.json({ | ||||
| @ -210,7 +211,7 @@ class LibraryItemController { | ||||
|     if (libraryItem.media.coverPath) { | ||||
|       libraryItem.updateMediaCover('') | ||||
|       await this.cacheManager.purgeCoverCache(libraryItem.id) | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
| @ -282,7 +283,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     libraryItem.media.updateAudioTracks(orderedFileData) | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| @ -309,7 +310,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)) | ||||
|     const itemsToDelete = Database.libraryItems.filter(li => libraryItemIds.includes(li.id)) | ||||
|     if (!itemsToDelete.length) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| @ -338,7 +339,7 @@ class LibraryItemController { | ||||
| 
 | ||||
|     for (let i = 0; i < updatePayloads.length; i++) { | ||||
|       var mediaPayload = updatePayloads[i].mediaPayload | ||||
|       var libraryItem = this.db.libraryItems.find(_li => _li.id === updatePayloads[i].id) | ||||
|       var libraryItem = Database.libraryItems.find(_li => _li.id === updatePayloads[i].id) | ||||
|       if (!libraryItem) return null | ||||
| 
 | ||||
|       await this.createAuthorsAndSeriesForItemUpdate(mediaPayload) | ||||
| @ -346,7 +347,7 @@ class LibraryItemController { | ||||
|       var hasUpdates = libraryItem.media.update(mediaPayload) | ||||
|       if (hasUpdates) { | ||||
|         Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) | ||||
|         await this.db.updateLibraryItem(libraryItem) | ||||
|         await Database.updateLibraryItem(libraryItem) | ||||
|         SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|         itemsUpdated++ | ||||
|       } | ||||
| @ -366,7 +367,7 @@ class LibraryItemController { | ||||
|     } | ||||
|     const libraryItems = [] | ||||
|     libraryItemIds.forEach((lid) => { | ||||
|       const li = this.db.libraryItems.find(_li => _li.id === lid) | ||||
|       const li = Database.libraryItems.find(_li => _li.id === lid) | ||||
|       if (li) libraryItems.push(li.toJSONExpanded()) | ||||
|     }) | ||||
|     res.json({ | ||||
| @ -389,7 +390,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| 
 | ||||
|     const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li) | ||||
|     const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li) | ||||
|     if (!libraryItems?.length) { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| @ -424,7 +425,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| 
 | ||||
|     const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li) | ||||
|     const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li) | ||||
|     if (!libraryItems?.length) { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| @ -441,15 +442,17 @@ class LibraryItemController { | ||||
|   } | ||||
| 
 | ||||
|   // DELETE: api/items/all
 | ||||
|   // TODO: Remove
 | ||||
|   async deleteAll(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|       Logger.warn('User other than admin attempted to delete all library items', req.user) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
|     Logger.info('Removing all Library Items') | ||||
|     var success = await this.db.recreateLibraryItemsDb() | ||||
|     if (success) res.sendStatus(200) | ||||
|     else res.sendStatus(500) | ||||
|     return res.sendStatus(404) | ||||
|     // if (!req.user.isAdminOrUp) {
 | ||||
|     //   Logger.warn('User other than admin attempted to delete all library items', req.user)
 | ||||
|     //   return res.sendStatus(403)
 | ||||
|     // }
 | ||||
|     // Logger.info('Removing all Library Items')
 | ||||
|     // var success = await this.db.recreateLibraryItemsDb()
 | ||||
|     // if (success) res.sendStatus(200)
 | ||||
|     // else res.sendStatus(500)
 | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/items/:id/scan (admin)
 | ||||
| @ -504,7 +507,7 @@ class LibraryItemController { | ||||
|     const chapters = req.body.chapters || [] | ||||
|     const wasUpdated = req.libraryItem.media.updateChapters(chapters) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateLibraryItem(req.libraryItem) | ||||
|       await Database.updateLibraryItem(req.libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
| @ -586,7 +589,7 @@ class LibraryItemController { | ||||
|       } | ||||
|     } | ||||
|     req.libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(req.libraryItem) | ||||
|     await Database.updateLibraryItem(req.libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| @ -682,13 +685,13 @@ class LibraryItemController { | ||||
|     } | ||||
| 
 | ||||
|     req.libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(req.libraryItem) | ||||
|     await Database.updateLibraryItem(req.libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     req.libraryItem = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!req.libraryItem?.media) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check user can access this library item
 | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| const { sort } = require('../libs/fastSort') | ||||
| const { isObject, toNumber } = require('../utils/index') | ||||
| const { toNumber } = require('../utils/index') | ||||
| 
 | ||||
| class MeController { | ||||
|   constructor() { } | ||||
| @ -33,7 +34,7 @@ class MeController { | ||||
| 
 | ||||
|   // GET: api/me/listening-stats
 | ||||
|   async getListeningStats(req, res) { | ||||
|     var listeningStats = await this.getUserListeningStatsHelpers(req.user.id) | ||||
|     const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) | ||||
|     res.json(listeningStats) | ||||
|   } | ||||
| 
 | ||||
| @ -51,21 +52,21 @@ class MeController { | ||||
|     if (!req.user.removeMediaProgress(req.params.id)) { | ||||
|       return res.sendStatus(200) | ||||
|     } | ||||
|     await this.db.updateEntity('user', req.user) | ||||
|     await Database.removeMediaProgress(req.params.id) | ||||
|     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/me/progress/:id
 | ||||
|   async createUpdateMediaProgress(req, res) { | ||||
|     var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id) | ||||
|     const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(404).send('Item not found') | ||||
|     } | ||||
| 
 | ||||
|     var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|     if (req.user.createUpdateMediaProgress(libraryItem, req.body)) { | ||||
|       const mediaProgress = req.user.getMediaProgress(libraryItem.id) | ||||
|       if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
| @ -73,8 +74,8 @@ class MeController { | ||||
| 
 | ||||
|   // PATCH: api/me/progress/:id/:episodeId
 | ||||
|   async createUpdateEpisodeMediaProgress(req, res) { | ||||
|     var episodeId = req.params.episodeId | ||||
|     var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id) | ||||
|     const episodeId = req.params.episodeId | ||||
|     const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(404).send('Item not found') | ||||
|     } | ||||
| @ -83,9 +84,9 @@ class MeController { | ||||
|       return res.status(404).send('Episode not found') | ||||
|     } | ||||
| 
 | ||||
|     var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|     if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) { | ||||
|       const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId) | ||||
|       if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
| @ -93,24 +94,26 @@ class MeController { | ||||
| 
 | ||||
|   // PATCH: api/me/progress/batch/update
 | ||||
|   async batchUpdateMediaProgress(req, res) { | ||||
|     var itemProgressPayloads = req.body | ||||
|     if (!itemProgressPayloads || !itemProgressPayloads.length) { | ||||
|     const itemProgressPayloads = req.body | ||||
|     if (!itemProgressPayloads?.length) { | ||||
|       return res.status(400).send('Missing request payload') | ||||
|     } | ||||
| 
 | ||||
|     var shouldUpdate = false | ||||
|     itemProgressPayloads.forEach((itemProgress) => { | ||||
|       var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
 | ||||
|     let shouldUpdate = false | ||||
|     for (const itemProgress of itemProgressPayloads) { | ||||
|       const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
 | ||||
|       if (libraryItem) { | ||||
|         var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId) | ||||
|         if (wasUpdated) shouldUpdate = true | ||||
|         if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) { | ||||
|           const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId) | ||||
|           if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||
|           shouldUpdate = true | ||||
|         } | ||||
|       } else { | ||||
|         Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`) | ||||
|       } | ||||
|     }) | ||||
|     } | ||||
| 
 | ||||
|     if (shouldUpdate) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
| 
 | ||||
| @ -119,18 +122,18 @@ class MeController { | ||||
| 
 | ||||
|   // POST: api/me/item/:id/bookmark
 | ||||
|   async createBookmark(req, res) { | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     var libraryItem = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!libraryItem) return res.sendStatus(404) | ||||
|     const { time, title } = req.body | ||||
|     var bookmark = req.user.createBookmark(libraryItem.id, time, title) | ||||
|     await this.db.updateEntity('user', req.user) | ||||
|     await Database.updateUser(req.user) | ||||
|     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     res.json(bookmark) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/me/item/:id/bookmark
 | ||||
|   async updateBookmark(req, res) { | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     var libraryItem = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!libraryItem) return res.sendStatus(404) | ||||
|     const { time, title } = req.body | ||||
|     if (!req.user.findBookmark(libraryItem.id, time)) { | ||||
| @ -139,14 +142,14 @@ class MeController { | ||||
|     } | ||||
|     var bookmark = req.user.updateBookmark(libraryItem.id, time, title) | ||||
|     if (!bookmark) return res.sendStatus(500) | ||||
|     await this.db.updateEntity('user', req.user) | ||||
|     await Database.updateUser(req.user) | ||||
|     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     res.json(bookmark) | ||||
|   } | ||||
| 
 | ||||
|   // DELETE: api/me/item/:id/bookmark/:time
 | ||||
|   async removeBookmark(req, res) { | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     var libraryItem = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!libraryItem) return res.sendStatus(404) | ||||
|     var time = Number(req.params.time) | ||||
|     if (isNaN(time)) return res.sendStatus(500) | ||||
| @ -156,7 +159,7 @@ class MeController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     req.user.removeBookmark(libraryItem.id, time) | ||||
|     await this.db.updateEntity('user', req.user) | ||||
|     await Database.updateUser(req.user) | ||||
|     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| @ -178,16 +181,16 @@ class MeController { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     const updatedLocalMediaProgress = [] | ||||
|     var numServerProgressUpdates = 0 | ||||
|     let numServerProgressUpdates = 0 | ||||
|     const updatedServerMediaProgress = [] | ||||
|     const localMediaProgress = req.body.localMediaProgress || [] | ||||
| 
 | ||||
|     localMediaProgress.forEach(localProgress => { | ||||
|     for (const localProgress of localMediaProgress) { | ||||
|       if (!localProgress.libraryItemId) { | ||||
|         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress) | ||||
|         return | ||||
|       } | ||||
|       var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId) | ||||
|       const libraryItem = Database.getLibraryItem(localProgress.libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress) | ||||
|         return | ||||
| @ -199,12 +202,14 @@ class MeController { | ||||
|         Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`) | ||||
|         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) | ||||
|         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) | ||||
|         if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||
|         updatedServerMediaProgress.push(mediaProgress) | ||||
|         numServerProgressUpdates++ | ||||
|       } else if (mediaProgress.lastUpdate < localProgress.lastUpdate) { | ||||
|         Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`) | ||||
|         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) | ||||
|         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) | ||||
|         if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||
|         updatedServerMediaProgress.push(mediaProgress) | ||||
|         numServerProgressUpdates++ | ||||
|       } else if (mediaProgress.lastUpdate > localProgress.lastUpdate) { | ||||
| @ -222,11 +227,10 @@ class MeController { | ||||
|       } else { | ||||
|         Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`) | ||||
|       } | ||||
|     }) | ||||
|     } | ||||
| 
 | ||||
|     Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`) | ||||
|     if (numServerProgressUpdates > 0) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
| 
 | ||||
| @ -244,7 +248,7 @@ class MeController { | ||||
|     let itemsInProgress = [] | ||||
|     for (const mediaProgress of req.user.mediaProgress) { | ||||
|       if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) { | ||||
|         const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId) | ||||
|         const libraryItem = Database.getLibraryItem(mediaProgress.libraryItemId) | ||||
|         if (libraryItem) { | ||||
|           if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') { | ||||
|             const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId) | ||||
| @ -274,7 +278,7 @@ class MeController { | ||||
| 
 | ||||
|   // GET: api/me/series/:id/remove-from-continue-listening
 | ||||
|   async removeSeriesFromContinueListening(req, res) { | ||||
|     const series = this.db.series.find(se => se.id === req.params.id) | ||||
|     const series = Database.series.find(se => se.id === req.params.id) | ||||
|     if (!series) { | ||||
|       Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`) | ||||
|       return res.sendStatus(404) | ||||
| @ -282,7 +286,7 @@ class MeController { | ||||
| 
 | ||||
|     const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       await Database.updateUser(req.user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.json(req.user.toJSONForBrowser()) | ||||
| @ -290,7 +294,7 @@ class MeController { | ||||
| 
 | ||||
|   // GET: api/me/series/:id/readd-to-continue-listening
 | ||||
|   async readdSeriesFromContinueListening(req, res) { | ||||
|     const series = this.db.series.find(se => se.id === req.params.id) | ||||
|     const series = Database.series.find(se => se.id === req.params.id) | ||||
|     if (!series) { | ||||
|       Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`) | ||||
|       return res.sendStatus(404) | ||||
| @ -298,7 +302,7 @@ class MeController { | ||||
| 
 | ||||
|     const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       await Database.updateUser(req.user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.json(req.user.toJSONForBrowser()) | ||||
| @ -308,7 +312,7 @@ class MeController { | ||||
|   async removeItemFromContinueListening(req, res) { | ||||
|     const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       await Database.updateUser(req.user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.json(req.user.toJSONForBrowser()) | ||||
|  | ||||
| @ -2,6 +2,7 @@ const Path = require('path') | ||||
| const fs = require('../libs/fsExtra') | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const filePerms = require('../utils/filePerms') | ||||
| const patternValidation = require('../libs/nodeCron/pattern-validation') | ||||
| @ -30,7 +31,7 @@ class MiscController { | ||||
|     var libraryId = req.body.library | ||||
|     var folderId = req.body.folder | ||||
| 
 | ||||
|     var library = this.db.libraries.find(lib => lib.id === libraryId) | ||||
|     var library = Database.libraries.find(lib => lib.id === libraryId) | ||||
|     if (!library) { | ||||
|       return res.status(404).send(`Library not found with id ${libraryId}`) | ||||
|     } | ||||
| @ -116,18 +117,18 @@ class MiscController { | ||||
|       return res.status(500).send('Invalid settings update object') | ||||
|     } | ||||
| 
 | ||||
|     var madeUpdates = this.db.serverSettings.update(settingsUpdate) | ||||
|     var madeUpdates = Database.serverSettings.update(settingsUpdate) | ||||
|     if (madeUpdates) { | ||||
|       // If backup schedule is updated - update backup manager
 | ||||
|       if (settingsUpdate.backupSchedule !== undefined) { | ||||
|         this.backupManager.updateCronSchedule() | ||||
|       } | ||||
| 
 | ||||
|       await this.db.updateServerSettings() | ||||
|       await Database.updateServerSettings() | ||||
|     } | ||||
|     return res.json({ | ||||
|       success: true, | ||||
|       serverSettings: this.db.serverSettings.toJSONForBrowser() | ||||
|       serverSettings: Database.serverSettings.toJSONForBrowser() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -147,7 +148,7 @@ class MiscController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     const tags = [] | ||||
|     this.db.libraryItems.forEach((li) => { | ||||
|     Database.libraryItems.forEach((li) => { | ||||
|       if (li.media.tags && li.media.tags.length) { | ||||
|         li.media.tags.forEach((tag) => { | ||||
|           if (!tags.includes(tag)) tags.push(tag) | ||||
| @ -176,7 +177,7 @@ class MiscController { | ||||
|     let tagMerged = false | ||||
|     let numItemsUpdated = 0 | ||||
| 
 | ||||
|     for (const li of this.db.libraryItems) { | ||||
|     for (const li of Database.libraryItems) { | ||||
|       if (!li.media.tags || !li.media.tags.length) continue | ||||
| 
 | ||||
|       if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
 | ||||
| @ -187,7 +188,7 @@ class MiscController { | ||||
|           li.media.tags.push(newTag) // Add new tag
 | ||||
|         } | ||||
|         Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`) | ||||
|         await this.db.updateLibraryItem(li) | ||||
|         await Database.updateLibraryItem(li) | ||||
|         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||
|         numItemsUpdated++ | ||||
|       } | ||||
| @ -209,13 +210,13 @@ class MiscController { | ||||
|     const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() | ||||
| 
 | ||||
|     let numItemsUpdated = 0 | ||||
|     for (const li of this.db.libraryItems) { | ||||
|     for (const li of Database.libraryItems) { | ||||
|       if (!li.media.tags || !li.media.tags.length) continue | ||||
| 
 | ||||
|       if (li.media.tags.includes(tag)) { | ||||
|         li.media.tags = li.media.tags.filter(t => t !== tag) | ||||
|         Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`) | ||||
|         await this.db.updateLibraryItem(li) | ||||
|         await Database.updateLibraryItem(li) | ||||
|         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||
|         numItemsUpdated++ | ||||
|       } | ||||
| @ -233,7 +234,7 @@ class MiscController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     const genres = [] | ||||
|     this.db.libraryItems.forEach((li) => { | ||||
|     Database.libraryItems.forEach((li) => { | ||||
|       if (li.media.metadata.genres && li.media.metadata.genres.length) { | ||||
|         li.media.metadata.genres.forEach((genre) => { | ||||
|           if (!genres.includes(genre)) genres.push(genre) | ||||
| @ -262,7 +263,7 @@ class MiscController { | ||||
|     let genreMerged = false | ||||
|     let numItemsUpdated = 0 | ||||
| 
 | ||||
|     for (const li of this.db.libraryItems) { | ||||
|     for (const li of Database.libraryItems) { | ||||
|       if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue | ||||
| 
 | ||||
|       if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
 | ||||
| @ -273,7 +274,7 @@ class MiscController { | ||||
|           li.media.metadata.genres.push(newGenre) // Add new genre
 | ||||
|         } | ||||
|         Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`) | ||||
|         await this.db.updateLibraryItem(li) | ||||
|         await Database.updateLibraryItem(li) | ||||
|         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||
|         numItemsUpdated++ | ||||
|       } | ||||
| @ -295,13 +296,13 @@ class MiscController { | ||||
|     const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() | ||||
| 
 | ||||
|     let numItemsUpdated = 0 | ||||
|     for (const li of this.db.libraryItems) { | ||||
|     for (const li of Database.libraryItems) { | ||||
|       if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue | ||||
| 
 | ||||
|       if (li.media.metadata.genres.includes(genre)) { | ||||
|         li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre) | ||||
|         Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`) | ||||
|         await this.db.updateLibraryItem(li) | ||||
|         await Database.updateLibraryItem(li) | ||||
|         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||
|         numItemsUpdated++ | ||||
|       } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const { version } = require('../../package.json') | ||||
| 
 | ||||
| class NotificationController { | ||||
| @ -7,14 +8,14 @@ class NotificationController { | ||||
|   get(req, res) { | ||||
|     res.json({ | ||||
|       data: this.notificationManager.getData(), | ||||
|       settings: this.db.notificationSettings | ||||
|       settings: Database.notificationSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async update(req, res) { | ||||
|     const updated = this.db.notificationSettings.update(req.body) | ||||
|     const updated = Database.notificationSettings.update(req.body) | ||||
|     if (updated) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|       await Database.updateSetting(Database.notificationSettings) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| @ -29,31 +30,31 @@ class NotificationController { | ||||
|   } | ||||
| 
 | ||||
|   async createNotification(req, res) { | ||||
|     const success = this.db.notificationSettings.createNotification(req.body) | ||||
|     const success = Database.notificationSettings.createNotification(req.body) | ||||
| 
 | ||||
|     if (success) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|       await Database.updateSetting(Database.notificationSettings) | ||||
|     } | ||||
|     res.json(this.db.notificationSettings) | ||||
|     res.json(Database.notificationSettings) | ||||
|   } | ||||
| 
 | ||||
|   async deleteNotification(req, res) { | ||||
|     if (this.db.notificationSettings.removeNotification(req.notification.id)) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|     if (Database.notificationSettings.removeNotification(req.notification.id)) { | ||||
|       await Database.updateSetting(Database.notificationSettings) | ||||
|     } | ||||
|     res.json(this.db.notificationSettings) | ||||
|     res.json(Database.notificationSettings) | ||||
|   } | ||||
| 
 | ||||
|   async updateNotification(req, res) { | ||||
|     const success = this.db.notificationSettings.updateNotification(req.body) | ||||
|     const success = Database.notificationSettings.updateNotification(req.body) | ||||
|     if (success) { | ||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|       await Database.updateSetting(Database.notificationSettings) | ||||
|     } | ||||
|     res.json(this.db.notificationSettings) | ||||
|     res.json(Database.notificationSettings) | ||||
|   } | ||||
| 
 | ||||
|   async sendNotificationTest(req, res) { | ||||
|     if (!this.db.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured') | ||||
|     if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured') | ||||
| 
 | ||||
|     const success = await this.notificationManager.sendTestNotification(req.notification) | ||||
|     if (success) res.sendStatus(200) | ||||
| @ -66,7 +67,7 @@ class NotificationController { | ||||
|     } | ||||
| 
 | ||||
|     if (req.params.id) { | ||||
|       const notification = this.db.notificationSettings.getNotification(req.params.id) | ||||
|       const notification = Database.notificationSettings.getNotification(req.params.id) | ||||
|       if (!notification) { | ||||
|         return res.sendStatus(404) | ||||
|       } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const Playlist = require('../objects/Playlist') | ||||
| 
 | ||||
| @ -14,8 +15,8 @@ class PlaylistController { | ||||
|     if (!success) { | ||||
|       return res.status(400).send('Invalid playlist request data') | ||||
|     } | ||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.insertEntity('playlist', newPlaylist) | ||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems) | ||||
|     await Database.createPlaylist(newPlaylist) | ||||
|     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
|   } | ||||
| @ -23,22 +24,22 @@ class PlaylistController { | ||||
|   // GET: api/playlists
 | ||||
|   findAllForUser(req, res) { | ||||
|     res.json({ | ||||
|       playlists: this.db.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(this.db.libraryItems)) | ||||
|       playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // GET: api/playlists/:id
 | ||||
|   findOne(req, res) { | ||||
|     res.json(req.playlist.toJSONExpanded(this.db.libraryItems)) | ||||
|     res.json(req.playlist.toJSONExpanded(Database.libraryItems)) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/playlists/:id
 | ||||
|   async update(req, res) { | ||||
|     const playlist = req.playlist | ||||
|     let wasUpdated = playlist.update(req.body) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('playlist', playlist) | ||||
|       await Database.updatePlaylist(playlist) | ||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||
|     } | ||||
|     res.json(jsonExpanded) | ||||
| @ -47,8 +48,8 @@ class PlaylistController { | ||||
|   // DELETE: api/playlists/:id
 | ||||
|   async delete(req, res) { | ||||
|     const playlist = req.playlist | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.removeEntity('playlist', playlist.id) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
|     await Database.removePlaylist(playlist.id) | ||||
|     SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| @ -62,7 +63,7 @@ class PlaylistController { | ||||
|       return res.status(400).send('Request body has no libraryItemId') | ||||
|     } | ||||
| 
 | ||||
|     const libraryItem = this.db.libraryItems.find(li => li.id === itemToAdd.libraryItemId) | ||||
|     const libraryItem = Database.libraryItems.find(li => li.id === itemToAdd.libraryItemId) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(400).send('Library item not found') | ||||
|     } | ||||
| @ -80,8 +81,16 @@ class PlaylistController { | ||||
|     } | ||||
| 
 | ||||
|     playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.updateEntity('playlist', playlist) | ||||
| 
 | ||||
|     const playlistMediaItem = { | ||||
|       playlistId: playlist.id, | ||||
|       mediaItemId: itemToAdd.episodeId || libraryItem.media.id, | ||||
|       mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book', | ||||
|       order: playlist.items.length | ||||
|     } | ||||
| 
 | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
|     await Database.createPlaylistMediaItem(playlistMediaItem) | ||||
|     SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
|   } | ||||
| @ -99,15 +108,15 @@ class PlaylistController { | ||||
| 
 | ||||
|     playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId) | ||||
| 
 | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|     // Playlist is removed when there are no items
 | ||||
|     if (!playlist.items.length) { | ||||
|       Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`) | ||||
|       await this.db.removeEntity('playlist', playlist.id) | ||||
|       await Database.removePlaylist(playlist.id) | ||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||
|     } else { | ||||
|       await this.db.updateEntity('playlist', playlist) | ||||
|       await Database.updatePlaylist(playlist) | ||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||
|     } | ||||
| 
 | ||||
| @ -122,20 +131,34 @@ class PlaylistController { | ||||
|     } | ||||
|     const itemsToAdd = req.body.items | ||||
|     let hasUpdated = false | ||||
| 
 | ||||
|     let order = playlist.items.length | ||||
|     const playlistMediaItems = [] | ||||
|     for (const item of itemsToAdd) { | ||||
|       if (!item.libraryItemId) { | ||||
|         return res.status(400).send('Item does not have libraryItemId') | ||||
|       } | ||||
| 
 | ||||
|       const libraryItem = Database.getLibraryItem(item.libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         return res.status(400).send('Item not found with id ' + item.libraryItemId) | ||||
|       } | ||||
| 
 | ||||
|       if (!playlist.containsItem(item)) { | ||||
|         playlistMediaItems.push({ | ||||
|           playlistId: playlist.id, | ||||
|           mediaItemId: item.episodeId || libraryItem.media.id, // podcastEpisodeId or bookId
 | ||||
|           mediaItemType: item.episodeId ? 'podcastEpisode' : 'book', | ||||
|           order: order++ | ||||
|         }) | ||||
|         playlist.addItem(item.libraryItemId, item.episodeId) | ||||
|         hasUpdated = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('playlist', playlist) | ||||
|       await Database.createBulkPlaylistMediaItems(playlistMediaItems) | ||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||
|     } | ||||
|     res.json(jsonExpanded) | ||||
| @ -153,21 +176,22 @@ class PlaylistController { | ||||
|       if (!item.libraryItemId) { | ||||
|         return res.status(400).send('Item does not have libraryItemId') | ||||
|       } | ||||
| 
 | ||||
|       if (playlist.containsItem(item)) { | ||||
|         playlist.removeItem(item.libraryItemId, item.episodeId) | ||||
|         hasUpdated = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) | ||||
|     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||
|     if (hasUpdated) { | ||||
|       // Playlist is removed when there are no items
 | ||||
|       if (!playlist.items.length) { | ||||
|         Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`) | ||||
|         await this.db.removeEntity('playlist', playlist.id) | ||||
|         await Database.removePlaylist(playlist.id) | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||
|       } else { | ||||
|         await this.db.updateEntity('playlist', playlist) | ||||
|         await Database.updatePlaylist(playlist) | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||
|       } | ||||
|     } | ||||
| @ -176,12 +200,12 @@ class PlaylistController { | ||||
| 
 | ||||
|   // POST: api/playlists/collection/:collectionId
 | ||||
|   async createFromCollection(req, res) { | ||||
|     let collection = this.db.collections.find(c => c.id === req.params.collectionId) | ||||
|     let collection = Database.collections.find(c => c.id === req.params.collectionId) | ||||
|     if (!collection) { | ||||
|       return res.status(404).send('Collection not found') | ||||
|     } | ||||
|     // Expand collection to get library items
 | ||||
|     collection = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     collection = collection.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|     // Filter out library items not accessible to user
 | ||||
|     const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item)) | ||||
| @ -201,15 +225,15 @@ class PlaylistController { | ||||
|     } | ||||
|     newPlaylist.setData(newPlaylistData) | ||||
| 
 | ||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems) | ||||
|     await this.db.insertEntity('playlist', newPlaylist) | ||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems) | ||||
|     await Database.createPlaylist(newPlaylist) | ||||
|     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) | ||||
|     res.json(jsonExpanded) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     if (req.params.id) { | ||||
|       const playlist = this.db.playlists.find(p => p.id === req.params.id) | ||||
|       const playlist = Database.playlists.find(p => p.id === req.params.id) | ||||
|       if (!playlist) { | ||||
|         return res.status(404).send('Playlist not found') | ||||
|       } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const fs = require('../libs/fsExtra') | ||||
| 
 | ||||
| @ -18,7 +19,7 @@ class PodcastController { | ||||
|     } | ||||
|     const payload = req.body | ||||
| 
 | ||||
|     const library = this.db.libraries.find(lib => lib.id === payload.libraryId) | ||||
|     const library = Database.libraries.find(lib => lib.id === payload.libraryId) | ||||
|     if (!library) { | ||||
|       Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) | ||||
|       return res.status(404).send('Library not found') | ||||
| @ -33,7 +34,7 @@ class PodcastController { | ||||
|     const podcastPath = filePathToPOSIX(payload.path) | ||||
| 
 | ||||
|     // Check if a library item with this podcast folder exists already
 | ||||
|     const existingLibraryItem = this.db.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id) | ||||
|     const existingLibraryItem = Database.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id) | ||||
|     if (existingLibraryItem) { | ||||
|       Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`) | ||||
|       return res.status(400).send('Podcast already exists') | ||||
| @ -80,7 +81,7 @@ class PodcastController { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     await this.db.insertLibraryItem(libraryItem) | ||||
|     await Database.createLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|     res.json(libraryItem.toJSONExpanded()) | ||||
| @ -199,7 +200,7 @@ class PodcastController { | ||||
|     const overrideDetails = req.query.override === '1' | ||||
|     const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails }) | ||||
|     if (episodesUpdated) { | ||||
|       await this.db.updateLibraryItem(req.libraryItem) | ||||
|       await Database.updateLibraryItem(req.libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
| @ -216,9 +217,8 @@ class PodcastController { | ||||
|       return res.status(404).send('Episode not found') | ||||
|     } | ||||
| 
 | ||||
|     var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|     if (libraryItem.media.updateEpisode(episodeId, req.body)) { | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
| @ -267,13 +267,13 @@ class PodcastController { | ||||
|       libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino) | ||||
|     } | ||||
| 
 | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     const item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     const item = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!item || !item.media) return res.sendStatus(404) | ||||
| 
 | ||||
|     if (!item.isPodcast) { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class RSSFeedController { | ||||
|   constructor() { } | ||||
| @ -8,7 +8,7 @@ class RSSFeedController { | ||||
|   async openRSSFeedForItem(req, res) { | ||||
|     const options = req.body || {} | ||||
| 
 | ||||
|     const item = this.db.libraryItems.find(li => li.id === req.params.itemId) | ||||
|     const item = Database.libraryItems.find(li => li.id === req.params.itemId) | ||||
|     if (!item) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check user can access this library item
 | ||||
| @ -45,7 +45,7 @@ class RSSFeedController { | ||||
|   async openRSSFeedForCollection(req, res) { | ||||
|     const options = req.body || {} | ||||
| 
 | ||||
|     const collection = this.db.collections.find(li => li.id === req.params.collectionId) | ||||
|     const collection = Database.collections.find(li => li.id === req.params.collectionId) | ||||
|     if (!collection) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check request body options exist
 | ||||
| @ -60,7 +60,7 @@ class RSSFeedController { | ||||
|       return res.status(400).send('Slug already in use') | ||||
|     } | ||||
| 
 | ||||
|     const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|     const collectionExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
|     const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length) | ||||
| 
 | ||||
|     // Check collection has audio tracks
 | ||||
| @ -79,7 +79,7 @@ class RSSFeedController { | ||||
|   async openRSSFeedForSeries(req, res) { | ||||
|     const options = req.body || {} | ||||
| 
 | ||||
|     const series = this.db.series.find(se => se.id === req.params.seriesId) | ||||
|     const series = Database.series.find(se => se.id === req.params.seriesId) | ||||
|     if (!series) return res.sendStatus(404) | ||||
| 
 | ||||
|     // Check request body options exist
 | ||||
| @ -96,7 +96,7 @@ class RSSFeedController { | ||||
| 
 | ||||
|     const seriesJson = series.toJSON() | ||||
|     // Get books in series that have audio tracks
 | ||||
|     seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
|     seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
| 
 | ||||
|     // Check series has audio tracks
 | ||||
|     if (!seriesJson.books.length) { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class SeriesController { | ||||
|   constructor() { } | ||||
| @ -35,7 +36,7 @@ class SeriesController { | ||||
|     var q = (req.query.q || '').toLowerCase() | ||||
|     if (!q) return res.json([]) | ||||
|     var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25 | ||||
|     var series = this.db.series.filter(se => se.name.toLowerCase().includes(q)) | ||||
|     var series = Database.series.filter(se => se.name.toLowerCase().includes(q)) | ||||
|     series = series.slice(0, limit) | ||||
|     res.json({ | ||||
|       results: series | ||||
| @ -45,17 +46,17 @@ class SeriesController { | ||||
|   async update(req, res) { | ||||
|     const hasUpdated = req.series.update(req.body) | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateEntity('series', req.series) | ||||
|       await Database.updateSeries(req.series) | ||||
|       SocketAuthority.emitter('series_updated', req.series.toJSON()) | ||||
|     } | ||||
|     res.json(req.series.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     const series = this.db.series.find(se => se.id === req.params.id) | ||||
|     const series = Database.series.find(se => se.id === req.params.id) | ||||
|     if (!series) return res.sendStatus(404) | ||||
| 
 | ||||
|     const libraryItemsInSeries = this.db.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id)) | ||||
|     const libraryItemsInSeries = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id)) | ||||
|     if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) { | ||||
|       Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user) | ||||
|       return res.sendStatus(403) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const { toNumber } = require('../utils/index') | ||||
| 
 | ||||
| class SessionController { | ||||
| @ -49,7 +50,7 @@ class SessionController { | ||||
|     } | ||||
| 
 | ||||
|     const openSessions = this.playbackSessionManager.sessions.map(se => { | ||||
|       const user = this.db.users.find(u => u.id === se.userId) || null | ||||
|       const user = Database.users.find(u => u.id === se.userId) || null | ||||
|       return { | ||||
|         ...se.toJSON(), | ||||
|         user: user ? { id: user.id, username: user.username } : null | ||||
| @ -62,7 +63,7 @@ class SessionController { | ||||
|   } | ||||
| 
 | ||||
|   getOpenSession(req, res) { | ||||
|     var libraryItem = this.db.getLibraryItem(req.session.libraryItemId) | ||||
|     var libraryItem = Database.getLibraryItem(req.session.libraryItemId) | ||||
|     var sessionForClient = req.session.toJSONForClient(libraryItem) | ||||
|     res.json(sessionForClient) | ||||
|   } | ||||
| @ -87,7 +88,7 @@ class SessionController { | ||||
|       await this.playbackSessionManager.removeSession(req.session.id) | ||||
|     } | ||||
| 
 | ||||
|     await this.db.removeEntity('session', req.session.id) | ||||
|     await Database.removePlaybackSession(req.session.id) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
| @ -115,7 +116,7 @@ class SessionController { | ||||
|   } | ||||
| 
 | ||||
|   async middleware(req, res, next) { | ||||
|     const playbackSession = await this.db.getPlaybackSession(req.params.id) | ||||
|     const playbackSession = await Database.getPlaybackSession(req.params.id) | ||||
|     if (!playbackSession) { | ||||
|       Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`) | ||||
|       return res.sendStatus(404) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class ToolsController { | ||||
|   constructor() { } | ||||
| @ -65,7 +66,7 @@ class ToolsController { | ||||
| 
 | ||||
|     const libraryItems = [] | ||||
|     for (const libraryItemId of libraryItemIds) { | ||||
|       const libraryItem = this.db.getLibraryItem(libraryItemId) | ||||
|       const libraryItem = Database.getLibraryItem(libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`) | ||||
|         return res.sendStatus(404) | ||||
| @ -105,7 +106,7 @@ class ToolsController { | ||||
|     } | ||||
| 
 | ||||
|     if (req.params.id) { | ||||
|       const item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|       const item = Database.libraryItems.find(li => li.id === req.params.id) | ||||
|       if (!item || !item.media) return res.sendStatus(404) | ||||
| 
 | ||||
|       // Check user can access this library item
 | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const User = require('../objects/user/User') | ||||
| 
 | ||||
| const { getId, toNumber } = require('../utils/index') | ||||
| const { toNumber } = require('../utils/index') | ||||
| 
 | ||||
| class UserController { | ||||
|   constructor() { } | ||||
| @ -15,11 +17,11 @@ class UserController { | ||||
|     const includes = (req.query.include || '').split(',').map(i => i.trim()) | ||||
| 
 | ||||
|     // Minimal toJSONForBrowser does not include mediaProgress and bookmarks
 | ||||
|     const users = this.db.users.map(u => u.toJSONForBrowser(hideRootToken, true)) | ||||
|     const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true)) | ||||
| 
 | ||||
|     if (includes.includes('latestSession')) { | ||||
|       for (const user of users) { | ||||
|         const userSessions = await this.db.selectUserSessions(user.id) | ||||
|         const userSessions = await Database.getPlaybackSessions({ userId: user.id }) | ||||
|         user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null | ||||
|       } | ||||
|     } | ||||
| @ -35,7 +37,7 @@ class UserController { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
| 
 | ||||
|     const user = this.db.users.find(u => u.id === req.params.id) | ||||
|     const user = Database.users.find(u => u.id === req.params.id) | ||||
|     if (!user) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| @ -47,18 +49,19 @@ class UserController { | ||||
|     var account = req.body | ||||
| 
 | ||||
|     var username = account.username | ||||
|     var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase()) | ||||
|     var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase()) | ||||
|     if (usernameExists) { | ||||
|       return res.status(500).send('Username already taken') | ||||
|     } | ||||
| 
 | ||||
|     account.id = getId('usr') | ||||
|     account.id = uuidv4() | ||||
|     account.pash = await this.auth.hashPass(account.password) | ||||
|     delete account.password | ||||
|     account.token = await this.auth.generateAccessToken({ userId: account.id, username }) | ||||
|     account.createdAt = Date.now() | ||||
|     var newUser = new User(account) | ||||
|     var success = await this.db.insertEntity('user', newUser) | ||||
|     const newUser = new User(account) | ||||
| 
 | ||||
|     const success = await Database.createUser(newUser) | ||||
|     if (success) { | ||||
|       SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) | ||||
|       res.json({ | ||||
| @ -81,7 +84,7 @@ class UserController { | ||||
|     var shouldUpdateToken = false | ||||
| 
 | ||||
|     if (account.username !== undefined && account.username !== user.username) { | ||||
|       var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase()) | ||||
|       var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase()) | ||||
|       if (usernameExists) { | ||||
|         return res.status(500).send('Username already taken') | ||||
|       } | ||||
| @ -94,13 +97,12 @@ class UserController { | ||||
|       delete account.password | ||||
|     } | ||||
| 
 | ||||
|     var hasUpdated = user.update(account) | ||||
|     if (hasUpdated) { | ||||
|     if (user.update(account)) { | ||||
|       if (shouldUpdateToken) { | ||||
|         user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) | ||||
|         Logger.info(`[UserController] User ${user.username} was generated a new api token`) | ||||
|       } | ||||
|       await this.db.updateEntity('user', user) | ||||
|       await Database.updateUser(user) | ||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) | ||||
|     } | ||||
| 
 | ||||
| @ -124,13 +126,13 @@ class UserController { | ||||
|     // Todo: check if user is logged in and cancel streams
 | ||||
| 
 | ||||
|     // Remove user playlists
 | ||||
|     const userPlaylists = this.db.playlists.filter(p => p.userId === user.id) | ||||
|     const userPlaylists = Database.playlists.filter(p => p.userId === user.id) | ||||
|     for (const playlist of userPlaylists) { | ||||
|       await this.db.removeEntity('playlist', playlist.id) | ||||
|       await Database.removePlaylist(playlist.id) | ||||
|     } | ||||
| 
 | ||||
|     const userJson = user.toJSONForBrowser() | ||||
|     await this.db.removeEntity('user', user.id) | ||||
|     await Database.removeUser(user.id) | ||||
|     SocketAuthority.adminEmitter('user_removed', userJson) | ||||
|     res.json({ | ||||
|       success: true | ||||
| @ -165,37 +167,39 @@ class UserController { | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/users/:id/purge-media-progress
 | ||||
|   // TODO: Remove
 | ||||
|   async purgeMediaProgress(req, res) { | ||||
|     const user = req.reqUser | ||||
|     return res.sendStatus(404) | ||||
|     // const user = req.reqUser
 | ||||
| 
 | ||||
|     if (user.type === 'root' && !req.user.isRoot) { | ||||
|       Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
|     // if (user.type === 'root' && !req.user.isRoot) {
 | ||||
|     //   Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
 | ||||
|     //   return res.sendStatus(403)
 | ||||
|     // }
 | ||||
| 
 | ||||
|     var progressPurged = 0 | ||||
|     user.mediaProgress = user.mediaProgress.filter(mp => { | ||||
|       const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         progressPurged++ | ||||
|         return false | ||||
|       } else if (mp.episodeId) { | ||||
|         const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null | ||||
|         if (!episode) { // Episode not found
 | ||||
|           progressPurged++ | ||||
|           return false | ||||
|         } | ||||
|       } | ||||
|       return true | ||||
|     }) | ||||
|     // var progressPurged = 0
 | ||||
|     // user.mediaProgress = user.mediaProgress.filter(mp => {
 | ||||
|     //   const libraryItem = Database.libraryItems.find(li => li.id === mp.libraryItemId)
 | ||||
|     //   if (!libraryItem) {
 | ||||
|     //     progressPurged++
 | ||||
|     //     return false
 | ||||
|     //   } else if (mp.episodeId) {
 | ||||
|     //     const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
 | ||||
|     //     if (!episode) { // Episode not found
 | ||||
|     //       progressPurged++
 | ||||
|     //       return false
 | ||||
|     //     }
 | ||||
|     //   }
 | ||||
|     //   return true
 | ||||
|     // })
 | ||||
| 
 | ||||
|     if (progressPurged) { | ||||
|       Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`) | ||||
|       await this.db.updateEntity('user', user) | ||||
|       SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser()) | ||||
|     } | ||||
|     // if (progressPurged) {
 | ||||
|     //   Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
 | ||||
|     //   await this.db.updateEntity('user', user)
 | ||||
|     //   SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
 | ||||
|     // }
 | ||||
| 
 | ||||
|     res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot)) | ||||
|     // res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
 | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/users/online (admin)
 | ||||
| @ -218,7 +222,7 @@ class UserController { | ||||
|     } | ||||
| 
 | ||||
|     if (req.params.id) { | ||||
|       req.reqUser = this.db.users.find(u => u.id === req.params.id) | ||||
|       req.reqUser = Database.users.find(u => u.id === req.params.id) | ||||
|       if (!req.reqUser) { | ||||
|         return res.sendStatus(404) | ||||
|       } | ||||
|  | ||||
							
								
								
									
										16
									
								
								server/controllers2/libraryItem.controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/controllers2/libraryItem.controller.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| const itemDb = require('../db/item.db') | ||||
| 
 | ||||
| const getLibraryItem = async (req, res) => { | ||||
|   let libraryItem = null | ||||
|   if (req.query.expanded == 1) { | ||||
|     libraryItem = await itemDb.getLibraryItemExpanded(req.params.id) | ||||
|   } else { | ||||
|     libraryItem = await itemDb.getLibraryItemMinified(req.params.id) | ||||
|   } | ||||
| 
 | ||||
|   res.json(libraryItem) | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|   getLibraryItem | ||||
| } | ||||
							
								
								
									
										80
									
								
								server/db/libraryItem.db.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/db/libraryItem.db.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| /** | ||||
|  * TODO: Unused for testing | ||||
|  */ | ||||
| const { Sequelize } = require('sequelize') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const getLibraryItemMinified = (libraryItemId) => { | ||||
|   return Database.models.libraryItem.findByPk(libraryItemId, { | ||||
|     include: [ | ||||
|       { | ||||
|         model: Database.models.book, | ||||
|         attributes: [ | ||||
|           'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags' | ||||
|         ], | ||||
|         include: [ | ||||
|           { | ||||
|             model: Database.models.author, | ||||
|             attributes: ['id', 'name'], | ||||
|             through: { | ||||
|               attributes: [] | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             model: Database.models.series, | ||||
|             attributes: ['id', 'name'], | ||||
|             through: { | ||||
|               attributes: ['sequence'] | ||||
|             } | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         model: Database.models.podcast, | ||||
|         attributes: [ | ||||
|           'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags', | ||||
|           [Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes'] | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| const getLibraryItemExpanded = (libraryItemId) => { | ||||
|   return Database.models.libraryItem.findByPk(libraryItemId, { | ||||
|     include: [ | ||||
|       { | ||||
|         model: Database.models.book, | ||||
|         include: [ | ||||
|           { | ||||
|             model: Database.models.author, | ||||
|             through: { | ||||
|               attributes: [] | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             model: Database.models.series, | ||||
|             through: { | ||||
|               attributes: ['sequence'] | ||||
|             } | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         model: Database.models.podcast, | ||||
|         include: [ | ||||
|           { | ||||
|             model: Database.models.podcastEpisode | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       'libraryFolder', | ||||
|       'library' | ||||
|     ] | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|   getLibraryItemMinified, | ||||
|   getLibraryItemExpanded | ||||
| } | ||||
| @ -10,8 +10,7 @@ const { writeConcatFile } = require('../utils/ffmpegHelpers') | ||||
| const toneHelpers = require('../utils/toneHelpers') | ||||
| 
 | ||||
| class AbMergeManager { | ||||
|   constructor(db, taskManager) { | ||||
|     this.db = db | ||||
|   constructor(taskManager) { | ||||
|     this.taskManager = taskManager | ||||
| 
 | ||||
|     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') | ||||
|  | ||||
| @ -10,8 +10,7 @@ const toneHelpers = require('../utils/toneHelpers') | ||||
| const Task = require('../objects/Task') | ||||
| 
 | ||||
| class AudioMetadataMangaer { | ||||
|   constructor(db, taskManager) { | ||||
|     this.db = db | ||||
|   constructor(taskManager) { | ||||
|     this.taskManager = taskManager | ||||
| 
 | ||||
|     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') | ||||
|  | ||||
| @ -10,15 +10,14 @@ const { downloadFile, filePathToPOSIX } = require('../utils/fileUtils') | ||||
| const { extractCoverArt } = require('../utils/ffmpegHelpers') | ||||
| 
 | ||||
| class CoverManager { | ||||
|   constructor(db, cacheManager) { | ||||
|     this.db = db | ||||
|   constructor(cacheManager) { | ||||
|     this.cacheManager = cacheManager | ||||
| 
 | ||||
|     this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items') | ||||
|   } | ||||
| 
 | ||||
|   getCoverDirectory(libraryItem) { | ||||
|     if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { | ||||
|     if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { | ||||
|       return libraryItem.path | ||||
|     } else { | ||||
|       return Path.posix.join(this.ItemMetadataPath, libraryItem.id) | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| const cron = require('../libs/nodeCron') | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| class CronManager { | ||||
|   constructor(db, scanner, podcastManager) { | ||||
|     this.db = db | ||||
|   constructor(scanner, podcastManager) { | ||||
|     this.scanner = scanner | ||||
|     this.podcastManager = podcastManager | ||||
| 
 | ||||
| @ -19,7 +19,7 @@ class CronManager { | ||||
|   } | ||||
| 
 | ||||
|   initLibraryScanCrons() { | ||||
|     for (const library of this.db.libraries) { | ||||
|     for (const library of Database.libraries) { | ||||
|       if (library.settings.autoScanCronExpression) { | ||||
|         this.startCronForLibrary(library) | ||||
|       } | ||||
| @ -64,7 +64,7 @@ class CronManager { | ||||
| 
 | ||||
|   initPodcastCrons() { | ||||
|     const cronExpressionMap = {} | ||||
|     this.db.libraryItems.forEach((li) => { | ||||
|     Database.libraryItems.forEach((li) => { | ||||
|       if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) { | ||||
|         if (!li.media.autoDownloadSchedule) { | ||||
|           Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`) | ||||
| @ -119,7 +119,7 @@ class CronManager { | ||||
|     // Get podcast library items to check
 | ||||
|     const libraryItems = [] | ||||
|     for (const libraryItemId of libraryItemIds) { | ||||
|       const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId) | ||||
|       const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`) | ||||
|         podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
 | ||||
|  | ||||
| @ -1,14 +1,11 @@ | ||||
| const nodemailer = require('nodemailer') | ||||
| const Logger = require("../Logger") | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| 
 | ||||
| class EmailManager { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
|   } | ||||
|   constructor() { } | ||||
| 
 | ||||
|   getTransporter() { | ||||
|     return nodemailer.createTransport(this.db.emailSettings.getTransportObject()) | ||||
|     return nodemailer.createTransport(Database.emailSettings.getTransportObject()) | ||||
|   } | ||||
| 
 | ||||
|   async sendTest(res) { | ||||
| @ -25,8 +22,8 @@ class EmailManager { | ||||
|     } | ||||
| 
 | ||||
|     transporter.sendMail({ | ||||
|       from: this.db.emailSettings.fromAddress, | ||||
|       to: this.db.emailSettings.testAddress || this.db.emailSettings.fromAddress, | ||||
|       from: Database.emailSettings.fromAddress, | ||||
|       to: Database.emailSettings.testAddress || Database.emailSettings.fromAddress, | ||||
|       subject: 'Test email from Audiobookshelf', | ||||
|       text: 'Success!' | ||||
|     }).then((result) => { | ||||
| @ -52,7 +49,7 @@ class EmailManager { | ||||
|     } | ||||
| 
 | ||||
|     transporter.sendMail({ | ||||
|       from: this.db.emailSettings.fromAddress, | ||||
|       from: Database.emailSettings.fromAddress, | ||||
|       to: device.email, | ||||
|       subject: "Here is your Ebook!", | ||||
|       html: '<div dir="auto"></div>', | ||||
|  | ||||
| @ -9,9 +9,7 @@ const Logger = require('../Logger') | ||||
| const TAG = '[LogManager]' | ||||
| 
 | ||||
| class LogManager { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily') | ||||
|     this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') | ||||
| 
 | ||||
| @ -20,12 +18,8 @@ class LogManager { | ||||
|     this.dailyLogFiles = [] | ||||
|   } | ||||
| 
 | ||||
|   get serverSettings() { | ||||
|     return this.db.serverSettings || {} | ||||
|   } | ||||
| 
 | ||||
|   get loggerDailyLogsToKeep() { | ||||
|     return this.serverSettings.loggerDailyLogsToKeep || 7 | ||||
|     return global.ServerSettings.loggerDailyLogsToKeep || 7 | ||||
|   } | ||||
| 
 | ||||
|   async ensureLogDirs() { | ||||
|  | ||||
| @ -1,12 +1,11 @@ | ||||
| const axios = require('axios') | ||||
| const Logger = require("../Logger") | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| const { notificationData } = require('../utils/notifications') | ||||
| 
 | ||||
| class NotificationManager { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.sendingNotification = false | ||||
|     this.notificationQueue = [] | ||||
|   } | ||||
| @ -16,10 +15,10 @@ class NotificationManager { | ||||
|   } | ||||
| 
 | ||||
|   onPodcastEpisodeDownloaded(libraryItem, episode) { | ||||
|     if (!this.db.notificationSettings.isUseable) return | ||||
|     if (!Database.notificationSettings.isUseable) return | ||||
| 
 | ||||
|     Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`) | ||||
|     const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId) | ||||
|     const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId) | ||||
|     const eventData = { | ||||
|       libraryItemId: libraryItem.id, | ||||
|       libraryId: libraryItem.libraryId, | ||||
| @ -42,19 +41,19 @@ class NotificationManager { | ||||
|   } | ||||
| 
 | ||||
|   async triggerNotification(eventName, eventData, intentionallyFail = false) { | ||||
|     if (!this.db.notificationSettings.isUseable) return | ||||
|     if (!Database.notificationSettings.isUseable) return | ||||
| 
 | ||||
|     // Will queue the notification if sendingNotification and queue is not full
 | ||||
|     if (!this.checkTriggerNotification(eventName, eventData)) return | ||||
| 
 | ||||
|     const notifications = this.db.notificationSettings.getActiveNotificationsForEvent(eventName) | ||||
|     const notifications = Database.notificationSettings.getActiveNotificationsForEvent(eventName) | ||||
|     for (const notification of notifications) { | ||||
|       Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`) | ||||
|       const success = intentionallyFail ? false : await this.sendNotification(notification, eventData) | ||||
| 
 | ||||
|       notification.updateNotificationFired(success) | ||||
|       if (!success) { // Failed notification
 | ||||
|         if (notification.numConsecutiveFailedAttempts >= this.db.notificationSettings.maxFailedAttempts) { | ||||
|         if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) { | ||||
|           Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`) | ||||
|           notification.enabled = false | ||||
|         } else { | ||||
| @ -63,8 +62,8 @@ class NotificationManager { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     await this.db.updateEntity('settings', this.db.notificationSettings) | ||||
|     SocketAuthority.emitter('notifications_updated', this.db.notificationSettings.toJSON()) | ||||
|     await Database.updateSetting(Database.notificationSettings) | ||||
|     SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON()) | ||||
| 
 | ||||
|     this.notificationFinished() | ||||
|   } | ||||
| @ -72,7 +71,7 @@ class NotificationManager { | ||||
|   // Return TRUE if notification should be triggered now
 | ||||
|   checkTriggerNotification(eventName, eventData) { | ||||
|     if (this.sendingNotification) { | ||||
|       if (this.notificationQueue.length >= this.db.notificationSettings.maxNotificationQueue) { | ||||
|       if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) { | ||||
|         Logger.warn(`[NotificationManager] Notification queue is full - ignoring event ${eventName}`) | ||||
|       } else { | ||||
|         Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`) | ||||
| @ -92,7 +91,7 @@ class NotificationManager { | ||||
|         const nextNotificationEvent = this.notificationQueue.shift() | ||||
|         this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData) | ||||
|       } | ||||
|     }, this.db.notificationSettings.notificationDelay) | ||||
|     }, Database.notificationSettings.notificationDelay) | ||||
|   } | ||||
| 
 | ||||
|   sendTestNotification(notification) { | ||||
| @ -107,7 +106,7 @@ class NotificationManager { | ||||
| 
 | ||||
|   sendNotification(notification, eventData) { | ||||
|     const payload = notification.getApprisePayload(eventData) | ||||
|     return axios.post(this.db.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => { | ||||
|     return axios.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => { | ||||
|       Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data) | ||||
|       return true | ||||
|     }).catch((error) => { | ||||
|  | ||||
| @ -2,6 +2,7 @@ const Path = require('path') | ||||
| const serverVersion = require('../../package.json').version | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const date = require('../libs/dateAndTime') | ||||
| const fs = require('../libs/fsExtra') | ||||
| @ -15,8 +16,7 @@ const DeviceInfo = require('../objects/DeviceInfo') | ||||
| const Stream = require('../objects/Stream') | ||||
| 
 | ||||
| class PlaybackSessionManager { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
|   constructor() { | ||||
|     this.StreamsPath = Path.join(global.MetadataPath, 'streams') | ||||
| 
 | ||||
|     this.sessions = [] | ||||
| @ -33,19 +33,31 @@ class PlaybackSessionManager { | ||||
|     return session?.stream || null | ||||
|   } | ||||
| 
 | ||||
|   getDeviceInfo(req) { | ||||
|   async getDeviceInfo(req) { | ||||
|     const ua = uaParserJs(req.headers['user-agent']) | ||||
|     const ip = requestIp.getClientIp(req) | ||||
| 
 | ||||
|     const clientDeviceInfo = req.body?.deviceInfo || null | ||||
| 
 | ||||
|     const deviceInfo = new DeviceInfo() | ||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion) | ||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user.id) | ||||
| 
 | ||||
|     if (clientDeviceInfo?.deviceId) { | ||||
|       const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId) | ||||
|       if (existingDevice) { | ||||
|         if (existingDevice.update(deviceInfo)) { | ||||
|           await Database.updateDevice(existingDevice) | ||||
|         } | ||||
|         return existingDevice | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     return deviceInfo | ||||
|   } | ||||
| 
 | ||||
|   async startSessionRequest(req, res, episodeId) { | ||||
|     const deviceInfo = this.getDeviceInfo(req) | ||||
|     const deviceInfo = await this.getDeviceInfo(req) | ||||
|     Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) | ||||
|     const { user, libraryItem, body: options } = req | ||||
|     const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) | ||||
| @ -77,7 +89,7 @@ class PlaybackSessionManager { | ||||
|   } | ||||
| 
 | ||||
|   async syncLocalSession(user, sessionJson) { | ||||
|     const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId) | ||||
|     const libraryItem = Database.getLibraryItem(sessionJson.libraryItemId) | ||||
|     const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null | ||||
|     if (!libraryItem || (libraryItem.isPodcast && !episode)) { | ||||
|       Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`) | ||||
| @ -88,12 +100,12 @@ class PlaybackSessionManager { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     let session = await this.db.getPlaybackSession(sessionJson.id) | ||||
|     let session = await Database.getPlaybackSession(sessionJson.id) | ||||
|     if (!session) { | ||||
|       // New session from local
 | ||||
|       session = new PlaybackSession(sessionJson) | ||||
|       Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) | ||||
|       await this.db.insertEntity('session', session) | ||||
|       await Database.createPlaybackSession(session) | ||||
|     } else { | ||||
|       session.currentTime = sessionJson.currentTime | ||||
|       session.timeListening = sessionJson.timeListening | ||||
| @ -102,7 +114,7 @@ class PlaybackSessionManager { | ||||
|       session.dayOfWeek = date.format(new Date(), 'dddd') | ||||
| 
 | ||||
|       Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`) | ||||
|       await this.db.updateEntity('session', session) | ||||
|       await Database.updatePlaybackSession(session) | ||||
|     } | ||||
| 
 | ||||
|     const result = { | ||||
| @ -126,8 +138,8 @@ class PlaybackSessionManager { | ||||
| 
 | ||||
|     // Update user and emit socket event
 | ||||
|     if (result.progressSynced) { | ||||
|       await this.db.updateEntity('user', user) | ||||
|       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) | ||||
|       if (itemProgress) await Database.upsertMediaProgress(itemProgress) | ||||
|       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { | ||||
|         id: itemProgress.id, | ||||
|         sessionId: session.id, | ||||
| @ -155,7 +167,7 @@ class PlaybackSessionManager { | ||||
| 
 | ||||
|   async startSession(user, deviceInfo, libraryItem, episodeId, options) { | ||||
|     // Close any sessions already open for user and device
 | ||||
|     const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.deviceId) | ||||
|     const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.id) | ||||
|     for (const session of userSessions) { | ||||
|       Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`) | ||||
|       await this.closeSession(user, session, null) | ||||
| @ -213,13 +225,13 @@ class PlaybackSessionManager { | ||||
|     user.currentSessionId = newPlaybackSession.id | ||||
| 
 | ||||
|     this.sessions.push(newPlaybackSession) | ||||
|     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) | ||||
|     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems)) | ||||
| 
 | ||||
|     return newPlaybackSession | ||||
|   } | ||||
| 
 | ||||
|   async syncSession(user, session, syncData) { | ||||
|     const libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId) | ||||
|     const libraryItem = Database.libraryItems.find(li => li.id === session.libraryItemId) | ||||
|     if (!libraryItem) { | ||||
|       Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) | ||||
|       return null | ||||
| @ -236,9 +248,8 @@ class PlaybackSessionManager { | ||||
|     } | ||||
|     const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) | ||||
|     if (wasUpdated) { | ||||
| 
 | ||||
|       await this.db.updateEntity('user', user) | ||||
|       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) | ||||
|       if (itemProgress) await Database.upsertMediaProgress(itemProgress) | ||||
|       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { | ||||
|         id: itemProgress.id, | ||||
|         sessionId: session.id, | ||||
| @ -259,7 +270,7 @@ class PlaybackSessionManager { | ||||
|       await this.saveSession(session) | ||||
|     } | ||||
|     Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`) | ||||
|     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) | ||||
|     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems)) | ||||
|     SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id) | ||||
|     return this.removeSession(session.id) | ||||
|   } | ||||
| @ -268,10 +279,10 @@ class PlaybackSessionManager { | ||||
|     if (!session.timeListening) return // Do not save a session with no listening time
 | ||||
| 
 | ||||
|     if (session.lastSave) { | ||||
|       return this.db.updateEntity('session', session) | ||||
|       return Database.updatePlaybackSession(session) | ||||
|     } else { | ||||
|       session.lastSave = Date.now() | ||||
|       return this.db.insertEntity('session', session) | ||||
|       return Database.createPlaybackSession(session) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -305,16 +316,5 @@ class PlaybackSessionManager { | ||||
|       Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Android app v0.9.54 and below had a bug where listening time was sending unix timestamp
 | ||||
|   //  See https://github.com/advplyr/audiobookshelf/issues/868
 | ||||
|   // Remove playback sessions with listening time too high
 | ||||
|   async removeInvalidSessions() { | ||||
|     const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000 | ||||
|     const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true) | ||||
|     if (numSessionsRemoved) { | ||||
|       Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = PlaybackSessionManager | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const fs = require('../libs/fsExtra') | ||||
| 
 | ||||
| @ -19,8 +20,7 @@ const AudioFile = require('../objects/files/AudioFile') | ||||
| const Task = require("../objects/Task") | ||||
| 
 | ||||
| class PodcastManager { | ||||
|   constructor(db, watcher, notificationManager, taskManager) { | ||||
|     this.db = db | ||||
|   constructor(watcher, notificationManager, taskManager) { | ||||
|     this.watcher = watcher | ||||
|     this.notificationManager = notificationManager | ||||
|     this.taskManager = taskManager | ||||
| @ -32,10 +32,6 @@ class PodcastManager { | ||||
|     this.MaxFailedEpisodeChecks = 24 | ||||
|   } | ||||
| 
 | ||||
|   get serverSettings() { | ||||
|     return this.db.serverSettings || {} | ||||
|   } | ||||
| 
 | ||||
|   getEpisodeDownloadsInQueue(libraryItemId) { | ||||
|     return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId) | ||||
|   } | ||||
| @ -59,6 +55,7 @@ class PodcastManager { | ||||
|       const newPe = new PodcastEpisode() | ||||
|       newPe.setData(ep, index++) | ||||
|       newPe.libraryItemId = libraryItem.id | ||||
|       newPe.podcastId = libraryItem.media.id | ||||
|       const newPeDl = new PodcastEpisodeDownload() | ||||
|       newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId) | ||||
|       this.startPodcastEpisodeDownload(newPeDl) | ||||
| @ -153,7 +150,7 @@ class PodcastManager { | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     const libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id) | ||||
|     const libraryItem = Database.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id) | ||||
|     if (!libraryItem) { | ||||
|       Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`) | ||||
|       return false | ||||
| @ -182,7 +179,7 @@ class PodcastManager { | ||||
|     } | ||||
| 
 | ||||
|     libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded() | ||||
|     podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded() | ||||
| @ -235,6 +232,7 @@ class PodcastManager { | ||||
|     } | ||||
|     const newAudioFile = new AudioFile() | ||||
|     newAudioFile.setDataFromProbe(libraryFile, mediaProbeData) | ||||
|     newAudioFile.index = 1 | ||||
|     return newAudioFile | ||||
|   } | ||||
| 
 | ||||
| @ -274,7 +272,7 @@ class PodcastManager { | ||||
| 
 | ||||
|     libraryItem.media.lastEpisodeCheck = Date.now() | ||||
|     libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     return libraryItem.media.autoDownloadEpisodes | ||||
|   } | ||||
| @ -313,7 +311,7 @@ class PodcastManager { | ||||
| 
 | ||||
|     libraryItem.media.lastEpisodeCheck = Date.now() | ||||
|     libraryItem.updatedAt = Date.now() | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     await Database.updateLibraryItem(libraryItem) | ||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|     return newEpisodes | ||||
|  | ||||
| @ -2,14 +2,13 @@ const Path = require('path') | ||||
| 
 | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| const fs = require('../libs/fsExtra') | ||||
| const Feed = require('../objects/Feed') | ||||
| 
 | ||||
| class RssFeedManager { | ||||
|   constructor(db) { | ||||
|     this.db = db | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.feeds = {} | ||||
|   } | ||||
| 
 | ||||
| @ -19,18 +18,18 @@ class RssFeedManager { | ||||
| 
 | ||||
|   validateFeedEntity(feedObj) { | ||||
|     if (feedObj.entityType === 'collection') { | ||||
|       if (!this.db.collections.some(li => li.id === feedObj.entityId)) { | ||||
|       if (!Database.collections.some(li => li.id === feedObj.entityId)) { | ||||
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`) | ||||
|         return false | ||||
|       } | ||||
|     } else if (feedObj.entityType === 'libraryItem') { | ||||
|       if (!this.db.libraryItems.some(li => li.id === feedObj.entityId)) { | ||||
|       if (!Database.libraryItems.some(li => li.id === feedObj.entityId)) { | ||||
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`) | ||||
|         return false | ||||
|       } | ||||
|     } else if (feedObj.entityType === 'series') { | ||||
|       const series = this.db.series.find(s => s.id === feedObj.entityId) | ||||
|       const hasSeriesBook = this.db.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
|       const series = Database.series.find(s => s.id === feedObj.entityId) | ||||
|       const hasSeriesBook = Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
|       if (!hasSeriesBook) { | ||||
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`) | ||||
|         return false | ||||
| @ -43,19 +42,13 @@ class RssFeedManager { | ||||
|   } | ||||
| 
 | ||||
|   async init() { | ||||
|     const feedObjects = await this.db.getAllEntities('feed') | ||||
|     if (!feedObjects || !feedObjects.length) return | ||||
|     const feedObjects = Database.feeds | ||||
|     if (!feedObjects?.length) return | ||||
| 
 | ||||
|     for (const feedObj of feedObjects) { | ||||
|       // Migration: In v2.2.12 entityType "item" was updated to "libraryItem"
 | ||||
|       if (feedObj.entityType === 'item') { | ||||
|         feedObj.entityType = 'libraryItem' | ||||
|         await this.db.updateEntity('feed', feedObj) | ||||
|       } | ||||
| 
 | ||||
|       // Remove invalid feeds
 | ||||
|       if (!this.validateFeedEntity(feedObj)) { | ||||
|         await this.db.removeEntity('feed', feedObj.id) | ||||
|         await Database.removeFeed(feedObj.id) | ||||
|       } | ||||
| 
 | ||||
|       const feed = new Feed(feedObj) | ||||
| @ -82,7 +75,7 @@ class RssFeedManager { | ||||
| 
 | ||||
|     // Check if feed needs to be updated
 | ||||
|     if (feed.entityType === 'libraryItem') { | ||||
|       const libraryItem = this.db.getLibraryItem(feed.entityId) | ||||
|       const libraryItem = Database.getLibraryItem(feed.entityId) | ||||
| 
 | ||||
|       let mostRecentlyUpdatedAt = libraryItem.updatedAt | ||||
|       if (libraryItem.isPodcast) { | ||||
| @ -94,12 +87,12 @@ class RssFeedManager { | ||||
|       if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) { | ||||
|         Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`) | ||||
|         feed.updateFromItem(libraryItem) | ||||
|         await this.db.updateEntity('feed', feed) | ||||
|         await Database.updateFeed(feed) | ||||
|       } | ||||
|     } else if (feed.entityType === 'collection') { | ||||
|       const collection = this.db.collections.find(c => c.id === feed.entityId) | ||||
|       const collection = Database.collections.find(c => c.id === feed.entityId) | ||||
|       if (collection) { | ||||
|         const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems) | ||||
|         const collectionExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||
| 
 | ||||
|         // Find most recently updated item in collection
 | ||||
|         let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate | ||||
| @ -113,15 +106,15 @@ class RssFeedManager { | ||||
|           Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) | ||||
| 
 | ||||
|           feed.updateFromCollection(collectionExpanded) | ||||
|           await this.db.updateEntity('feed', feed) | ||||
|           await Database.updateFeed(feed) | ||||
|         } | ||||
|       } | ||||
|     } else if (feed.entityType === 'series') { | ||||
|       const series = this.db.series.find(s => s.id === feed.entityId) | ||||
|       const series = Database.series.find(s => s.id === feed.entityId) | ||||
|       if (series) { | ||||
|         const seriesJson = series.toJSON() | ||||
|         // Get books in series that have audio tracks
 | ||||
|         seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
|         seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||
| 
 | ||||
|         // Find most recently updated item in series
 | ||||
|         let mostRecentlyUpdatedAt = seriesJson.updatedAt | ||||
| @ -140,7 +133,7 @@ class RssFeedManager { | ||||
|           Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`) | ||||
| 
 | ||||
|           feed.updateFromSeries(seriesJson) | ||||
|           await this.db.updateEntity('feed', feed) | ||||
|           await Database.updateFeed(feed) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @ -197,7 +190,7 @@ class RssFeedManager { | ||||
|     this.feeds[feed.id] = feed | ||||
| 
 | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) | ||||
|     await this.db.insertEntity('feed', feed) | ||||
|     await Database.createFeed(feed) | ||||
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||
|     return feed | ||||
|   } | ||||
| @ -214,7 +207,7 @@ class RssFeedManager { | ||||
|     this.feeds[feed.id] = feed | ||||
| 
 | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) | ||||
|     await this.db.insertEntity('feed', feed) | ||||
|     await Database.createFeed(feed) | ||||
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||
|     return feed | ||||
|   } | ||||
| @ -231,14 +224,14 @@ class RssFeedManager { | ||||
|     this.feeds[feed.id] = feed | ||||
| 
 | ||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) | ||||
|     await this.db.insertEntity('feed', feed) | ||||
|     await Database.createFeed(feed) | ||||
|     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||
|     return feed | ||||
|   } | ||||
| 
 | ||||
|   async handleCloseFeed(feed) { | ||||
|     if (!feed) return | ||||
|     await this.db.removeEntity('feed', feed.id) | ||||
|     await Database.removeFeed(feed.id) | ||||
|     SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified()) | ||||
|     delete this.feeds[feed.id] | ||||
|     Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`) | ||||
|  | ||||
							
								
								
									
										78
									
								
								server/models/Author.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								server/models/Author.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldAuthor = require('../objects/entities/Author') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Author extends Model { | ||||
|     static async getOldAuthors() { | ||||
|       const authors = await this.findAll() | ||||
|       return authors.map(au => au.getOldAuthor()) | ||||
|     } | ||||
| 
 | ||||
|     getOldAuthor() { | ||||
|       return new oldAuthor({ | ||||
|         id: this.id, | ||||
|         asin: this.asin, | ||||
|         name: this.name, | ||||
|         description: this.description, | ||||
|         imagePath: this.imagePath, | ||||
|         addedAt: this.createdAt.valueOf(), | ||||
|         updatedAt: this.updatedAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldAuthor) { | ||||
|       const author = this.getFromOld(oldAuthor) | ||||
|       return this.update(author, { | ||||
|         where: { | ||||
|           id: author.id | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldAuthor) { | ||||
|       const author = this.getFromOld(oldAuthor) | ||||
|       return this.create(author) | ||||
|     } | ||||
| 
 | ||||
|     static createBulkFromOld(oldAuthors) { | ||||
|       const authors = oldAuthors.map(this.getFromOld) | ||||
|       return this.bulkCreate(authors) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldAuthor) { | ||||
|       return { | ||||
|         id: oldAuthor.id, | ||||
|         name: oldAuthor.name, | ||||
|         asin: oldAuthor.asin, | ||||
|         description: oldAuthor.description, | ||||
|         imagePath: oldAuthor.imagePath | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(authorId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: authorId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Author.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     name: DataTypes.STRING, | ||||
|     asin: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT, | ||||
|     imagePath: DataTypes.STRING | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'author' | ||||
|   }) | ||||
| 
 | ||||
|   return Author | ||||
| } | ||||
							
								
								
									
										121
									
								
								server/models/Book.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								server/models/Book.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,121 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| const Logger = require('../Logger') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Book extends Model { | ||||
|     static getOldBook(libraryItemExpanded) { | ||||
|       const bookExpanded = libraryItemExpanded.media | ||||
|       const authors = bookExpanded.authors.map(au => { | ||||
|         return { | ||||
|           id: au.id, | ||||
|           name: au.name | ||||
|         } | ||||
|       }) | ||||
|       const series = bookExpanded.series.map(se => { | ||||
|         return { | ||||
|           id: se.id, | ||||
|           name: se.name, | ||||
|           sequence: se.bookSeries.sequence | ||||
|         } | ||||
|       }) | ||||
|       return { | ||||
|         id: bookExpanded.id, | ||||
|         libraryItemId: libraryItemExpanded.id, | ||||
|         coverPath: bookExpanded.coverPath, | ||||
|         tags: bookExpanded.tags, | ||||
|         audioFiles: bookExpanded.audioFiles, | ||||
|         chapters: bookExpanded.chapters, | ||||
|         ebookFile: bookExpanded.ebookFile, | ||||
|         metadata: { | ||||
|           title: bookExpanded.title, | ||||
|           subtitle: bookExpanded.subtitle, | ||||
|           authors: authors, | ||||
|           narrators: bookExpanded.narrators, | ||||
|           series: series, | ||||
|           genres: bookExpanded.genres, | ||||
|           publishedYear: bookExpanded.publishedYear, | ||||
|           publishedDate: bookExpanded.publishedDate, | ||||
|           publisher: bookExpanded.publisher, | ||||
|           description: bookExpanded.description, | ||||
|           isbn: bookExpanded.isbn, | ||||
|           asin: bookExpanded.asin, | ||||
|           language: bookExpanded.language, | ||||
|           explicit: bookExpanded.explicit, | ||||
|           abridged: bookExpanded.abridged | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {object} oldBook  | ||||
|      * @returns {boolean} true if updated | ||||
|      */ | ||||
|     static saveFromOld(oldBook) { | ||||
|       const book = this.getFromOld(oldBook) | ||||
|       return this.update(book, { | ||||
|         where: { | ||||
|           id: book.id | ||||
|         } | ||||
|       }).then(result => result[0] > 0).catch((error) => { | ||||
|         Logger.error(`[Book] Failed to save book ${book.id}`, error) | ||||
|         return false | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldBook) { | ||||
|       return { | ||||
|         id: oldBook.id, | ||||
|         title: oldBook.metadata.title, | ||||
|         subtitle: oldBook.metadata.subtitle, | ||||
|         publishedYear: oldBook.metadata.publishedYear, | ||||
|         publishedDate: oldBook.metadata.publishedDate, | ||||
|         publisher: oldBook.metadata.publisher, | ||||
|         description: oldBook.metadata.description, | ||||
|         isbn: oldBook.metadata.isbn, | ||||
|         asin: oldBook.metadata.asin, | ||||
|         language: oldBook.metadata.language, | ||||
|         explicit: !!oldBook.metadata.explicit, | ||||
|         abridged: !!oldBook.metadata.abridged, | ||||
|         narrators: oldBook.metadata.narrators, | ||||
|         ebookFile: oldBook.ebookFile?.toJSON() || null, | ||||
|         coverPath: oldBook.coverPath, | ||||
|         audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [], | ||||
|         chapters: oldBook.chapters, | ||||
|         tags: oldBook.tags, | ||||
|         genres: oldBook.metadata.genres | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Book.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     title: DataTypes.STRING, | ||||
|     subtitle: DataTypes.STRING, | ||||
|     publishedYear: DataTypes.STRING, | ||||
|     publishedDate: DataTypes.STRING, | ||||
|     publisher: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT, | ||||
|     isbn: DataTypes.STRING, | ||||
|     asin: DataTypes.STRING, | ||||
|     language: DataTypes.STRING, | ||||
|     explicit: DataTypes.BOOLEAN, | ||||
|     abridged: DataTypes.BOOLEAN, | ||||
|     coverPath: DataTypes.STRING, | ||||
| 
 | ||||
|     narrators: DataTypes.JSON, | ||||
|     audioFiles: DataTypes.JSON, | ||||
|     ebookFile: DataTypes.JSON, | ||||
|     chapters: DataTypes.JSON, | ||||
|     tags: DataTypes.JSON, | ||||
|     genres: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'book' | ||||
|   }) | ||||
| 
 | ||||
|   return Book | ||||
| } | ||||
							
								
								
									
										40
									
								
								server/models/BookAuthor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								server/models/BookAuthor.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class BookAuthor extends Model { | ||||
|     static removeByIds(authorId = null, bookId = null) { | ||||
|       const where = {} | ||||
|       if (authorId) where.authorId = authorId | ||||
|       if (bookId) where.bookId = bookId | ||||
|       return this.destroy({ | ||||
|         where | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   BookAuthor.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     } | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'bookAuthor', | ||||
|     timestamps: false | ||||
|   }) | ||||
| 
 | ||||
|   // Super Many-to-Many
 | ||||
|   // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
 | ||||
|   const { book, author } = sequelize.models | ||||
|   book.belongsToMany(author, { through: BookAuthor }) | ||||
|   author.belongsToMany(book, { through: BookAuthor }) | ||||
| 
 | ||||
|   book.hasMany(BookAuthor) | ||||
|   BookAuthor.belongsTo(book) | ||||
| 
 | ||||
|   author.hasMany(BookAuthor) | ||||
|   BookAuthor.belongsTo(author) | ||||
| 
 | ||||
|   return BookAuthor | ||||
| } | ||||
							
								
								
									
										41
									
								
								server/models/BookSeries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								server/models/BookSeries.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class BookSeries extends Model { | ||||
|     static removeByIds(seriesId = null, bookId = null) { | ||||
|       const where = {} | ||||
|       if (seriesId) where.seriesId = seriesId | ||||
|       if (bookId) where.bookId = bookId | ||||
|       return this.destroy({ | ||||
|         where | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   BookSeries.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     sequence: DataTypes.STRING | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'bookSeries', | ||||
|     timestamps: false | ||||
|   }) | ||||
| 
 | ||||
|   // Super Many-to-Many
 | ||||
|   // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
 | ||||
|   const { book, series } = sequelize.models | ||||
|   book.belongsToMany(series, { through: BookSeries }) | ||||
|   series.belongsToMany(book, { through: BookSeries }) | ||||
| 
 | ||||
|   book.hasMany(BookSeries) | ||||
|   BookSeries.belongsTo(book) | ||||
| 
 | ||||
|   series.hasMany(BookSeries) | ||||
|   BookSeries.belongsTo(series) | ||||
| 
 | ||||
|   return BookSeries | ||||
| } | ||||
							
								
								
									
										117
									
								
								server/models/Collection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								server/models/Collection.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldCollection = require('../objects/Collection') | ||||
| const { areEquivalent } = require('../utils/index') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Collection extends Model { | ||||
|     static async getOldCollections() { | ||||
|       const collections = await this.findAll({ | ||||
|         include: { | ||||
|           model: sequelize.models.book, | ||||
|           include: sequelize.models.libraryItem | ||||
|         } | ||||
|       }) | ||||
|       return collections.map(c => this.getOldCollection(c)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldCollection(collectionExpanded) { | ||||
|       const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || [] | ||||
|       return new oldCollection({ | ||||
|         id: collectionExpanded.id, | ||||
|         libraryId: collectionExpanded.libraryId, | ||||
|         name: collectionExpanded.name, | ||||
|         description: collectionExpanded.description, | ||||
|         books: libraryItemIds, | ||||
|         lastUpdate: collectionExpanded.updatedAt.valueOf(), | ||||
|         createdAt: collectionExpanded.createdAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldCollection) { | ||||
|       const collection = this.getFromOld(oldCollection) | ||||
|       return this.create(collection) | ||||
|     } | ||||
| 
 | ||||
|     static async fullUpdateFromOld(oldCollection, collectionBooks) { | ||||
|       const existingCollection = await this.findByPk(oldCollection.id, { | ||||
|         include: sequelize.models.collectionBook | ||||
|       }) | ||||
|       if (!existingCollection) return false | ||||
| 
 | ||||
|       let hasUpdates = false | ||||
|       const collection = this.getFromOld(oldCollection) | ||||
| 
 | ||||
|       for (const cb of collectionBooks) { | ||||
|         const existingCb = existingCollection.collectionBooks.find(i => i.bookId === cb.bookId) | ||||
|         if (!existingCb) { | ||||
|           await sequelize.models.collectionBook.create(cb) | ||||
|           hasUpdates = true | ||||
|         } else if (existingCb.order != cb.order) { | ||||
|           await existingCb.update({ order: cb.order }) | ||||
|           hasUpdates = true | ||||
|         } | ||||
|       } | ||||
|       for (const cb of existingCollection.collectionBooks) { | ||||
|         // collectionBook was removed
 | ||||
|         if (!collectionBooks.some(i => i.bookId === cb.bookId)) { | ||||
|           await cb.destroy() | ||||
|           hasUpdates = true | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       let hasCollectionUpdates = false | ||||
|       for (const key in collection) { | ||||
|         let existingValue = existingCollection[key] | ||||
|         if (existingValue instanceof Date) existingValue = existingValue.valueOf() | ||||
|         if (!areEquivalent(collection[key], existingValue)) { | ||||
|           hasCollectionUpdates = true | ||||
|         } | ||||
|       } | ||||
|       if (hasCollectionUpdates) { | ||||
|         existingCollection.update(collection) | ||||
|         hasUpdates = true | ||||
|       } | ||||
|       return hasUpdates | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldCollection) { | ||||
|       return { | ||||
|         id: oldCollection.id, | ||||
|         name: oldCollection.name, | ||||
|         description: oldCollection.description, | ||||
|         createdAt: oldCollection.createdAt, | ||||
|         updatedAt: oldCollection.lastUpdate, | ||||
|         libraryId: oldCollection.libraryId | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(collectionId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: collectionId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Collection.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     name: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'collection' | ||||
|   }) | ||||
| 
 | ||||
|   const { library } = sequelize.models | ||||
| 
 | ||||
|   library.hasMany(Collection) | ||||
|   Collection.belongsTo(library) | ||||
| 
 | ||||
|   return Collection | ||||
| } | ||||
							
								
								
									
										42
									
								
								server/models/CollectionBook.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/models/CollectionBook.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class CollectionBook extends Model { | ||||
|     static removeByIds(collectionId, bookId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           bookId, | ||||
|           collectionId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   CollectionBook.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     order: DataTypes.INTEGER | ||||
|   }, { | ||||
|     sequelize, | ||||
|     timestamps: true, | ||||
|     updatedAt: false, | ||||
|     modelName: 'collectionBook' | ||||
|   }) | ||||
| 
 | ||||
|   // Super Many-to-Many
 | ||||
|   // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
 | ||||
|   const { book, collection } = sequelize.models | ||||
|   book.belongsToMany(collection, { through: CollectionBook }) | ||||
|   collection.belongsToMany(book, { through: CollectionBook }) | ||||
| 
 | ||||
|   book.hasMany(CollectionBook) | ||||
|   CollectionBook.belongsTo(book) | ||||
| 
 | ||||
|   collection.hasMany(CollectionBook) | ||||
|   CollectionBook.belongsTo(collection) | ||||
| 
 | ||||
|   return CollectionBook | ||||
| } | ||||
							
								
								
									
										112
									
								
								server/models/Device.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								server/models/Device.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| const oldDevice = require('../objects/DeviceInfo') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Device extends Model { | ||||
|     getOldDevice() { | ||||
|       let browserVersion = null | ||||
|       let sdkVersion = null | ||||
|       if (this.clientName === 'Abs Android') { | ||||
|         sdkVersion = this.deviceVersion || null | ||||
|       } else { | ||||
|         browserVersion = this.deviceVersion || null | ||||
|       } | ||||
| 
 | ||||
|       return new oldDevice({ | ||||
|         id: this.id, | ||||
|         deviceId: this.deviceId, | ||||
|         userId: this.userId, | ||||
|         ipAddress: this.ipAddress, | ||||
|         browserName: this.extraData.browserName || null, | ||||
|         browserVersion, | ||||
|         osName: this.extraData.osName || null, | ||||
|         osVersion: this.extraData.osVersion || null, | ||||
|         clientVersion: this.clientVersion || null, | ||||
|         manufacturer: this.extraData.manufacturer || null, | ||||
|         model: this.extraData.model || null, | ||||
|         sdkVersion, | ||||
|         deviceName: this.deviceName, | ||||
|         clientName: this.clientName | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static async getOldDeviceByDeviceId(deviceId) { | ||||
|       const device = await this.findOne({ | ||||
|         deviceId | ||||
|       }) | ||||
|       if (!device) return null | ||||
|       return device.getOldDevice() | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldDevice) { | ||||
|       const device = this.getFromOld(oldDevice) | ||||
|       return this.create(device) | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldDevice) { | ||||
|       const device = this.getFromOld(oldDevice) | ||||
|       return this.update(device, { | ||||
|         where: { | ||||
|           id: device.id | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldDeviceInfo) { | ||||
|       let extraData = {} | ||||
| 
 | ||||
|       if (oldDeviceInfo.manufacturer) { | ||||
|         extraData.manufacturer = oldDeviceInfo.manufacturer | ||||
|       } | ||||
|       if (oldDeviceInfo.model) { | ||||
|         extraData.model = oldDeviceInfo.model | ||||
|       } | ||||
|       if (oldDeviceInfo.osName) { | ||||
|         extraData.osName = oldDeviceInfo.osName | ||||
|       } | ||||
|       if (oldDeviceInfo.osVersion) { | ||||
|         extraData.osVersion = oldDeviceInfo.osVersion | ||||
|       } | ||||
|       if (oldDeviceInfo.browserName) { | ||||
|         extraData.browserName = oldDeviceInfo.browserName | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         id: oldDeviceInfo.id, | ||||
|         deviceId: oldDeviceInfo.deviceId, | ||||
|         clientName: oldDeviceInfo.clientName || null, | ||||
|         clientVersion: oldDeviceInfo.clientVersion || null, | ||||
|         ipAddress: oldDeviceInfo.ipAddress, | ||||
|         deviceName: oldDeviceInfo.deviceName || null, | ||||
|         deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null, | ||||
|         userId: oldDeviceInfo.userId, | ||||
|         extraData | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Device.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     deviceId: DataTypes.STRING, | ||||
|     clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
 | ||||
|     clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
 | ||||
|     ipAddress: DataTypes.STRING, | ||||
|     deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
 | ||||
|     deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK
 | ||||
|     extraData: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'device' | ||||
|   }) | ||||
| 
 | ||||
|   const { user } = sequelize.models | ||||
| 
 | ||||
|   user.hasMany(Device) | ||||
|   Device.belongsTo(user) | ||||
| 
 | ||||
|   return Device | ||||
| } | ||||
							
								
								
									
										165
									
								
								server/models/Feed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								server/models/Feed.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| const oldFeed = require('../objects/Feed') | ||||
| /* | ||||
|  * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
 | ||||
|  * Feeds can be created from LibraryItem, Collection, Playlist or Series | ||||
|  */ | ||||
| module.exports = (sequelize) => { | ||||
|   class Feed extends Model { | ||||
|     static async getOldFeeds() { | ||||
|       const feeds = await this.findAll({ | ||||
|         include: { | ||||
|           model: sequelize.models.feedEpisode | ||||
|         } | ||||
|       }) | ||||
|       return feeds.map(f => this.getOldFeed(f)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldFeed(feedExpanded) { | ||||
|       const episodes = feedExpanded.feedEpisodes.map((feedEpisode) => feedEpisode.getOldEpisode()) | ||||
| 
 | ||||
|       return new oldFeed({ | ||||
|         id: feedExpanded.id, | ||||
|         slug: feedExpanded.slug, | ||||
|         userId: feedExpanded.userId, | ||||
|         entityType: feedExpanded.entityType, | ||||
|         entityId: feedExpanded.entityId, | ||||
|         meta: { | ||||
|           title: feedExpanded.title, | ||||
|           description: feedExpanded.description, | ||||
|           author: feedExpanded.author, | ||||
|           imageUrl: feedExpanded.imageURL, | ||||
|           feedUrl: feedExpanded.feedURL, | ||||
|           link: feedExpanded.siteURL, | ||||
|           explicit: feedExpanded.explicit, | ||||
|           type: feedExpanded.podcastType, | ||||
|           language: feedExpanded.language, | ||||
|           preventIndexing: feedExpanded.preventIndexing, | ||||
|           ownerName: feedExpanded.ownerName, | ||||
|           ownerEmail: feedExpanded.ownerEmail | ||||
|         }, | ||||
|         serverAddress: feedExpanded.serverAddress, | ||||
|         feedUrl: feedExpanded.feedURL, | ||||
|         episodes, | ||||
|         createdAt: feedExpanded.createdAt.valueOf(), | ||||
|         updatedAt: feedExpanded.updatedAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static removeById(feedId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: feedId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     getEntity(options) { | ||||
|       if (!this.entityType) return Promise.resolve(null) | ||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}` | ||||
|       return this[mixinMethodName](options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Feed.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     slug: DataTypes.STRING, | ||||
|     entityType: DataTypes.STRING, | ||||
|     entityId: DataTypes.UUIDV4, | ||||
|     entityUpdatedAt: DataTypes.DATE, | ||||
|     serverAddress: DataTypes.STRING, | ||||
|     feedURL: DataTypes.STRING, | ||||
|     imageURL: DataTypes.STRING, | ||||
|     siteURL: DataTypes.STRING, | ||||
|     title: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT, | ||||
|     author: DataTypes.STRING, | ||||
|     podcastType: DataTypes.STRING, | ||||
|     language: DataTypes.STRING, | ||||
|     ownerName: DataTypes.STRING, | ||||
|     ownerEmail: DataTypes.STRING, | ||||
|     explicit: DataTypes.BOOLEAN, | ||||
|     preventIndexing: DataTypes.BOOLEAN | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'feed' | ||||
|   }) | ||||
| 
 | ||||
|   const { user, libraryItem, collection, series, playlist } = sequelize.models | ||||
| 
 | ||||
|   user.hasMany(Feed) | ||||
|   Feed.belongsTo(user) | ||||
| 
 | ||||
|   libraryItem.hasMany(Feed, { | ||||
|     foreignKey: 'entityId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       entityType: 'libraryItem' | ||||
|     } | ||||
|   }) | ||||
|   Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false }) | ||||
| 
 | ||||
|   collection.hasMany(Feed, { | ||||
|     foreignKey: 'entityId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       entityType: 'collection' | ||||
|     } | ||||
|   }) | ||||
|   Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false }) | ||||
| 
 | ||||
|   series.hasMany(Feed, { | ||||
|     foreignKey: 'entityId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       entityType: 'series' | ||||
|     } | ||||
|   }) | ||||
|   Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false }) | ||||
| 
 | ||||
|   playlist.hasMany(Feed, { | ||||
|     foreignKey: 'entityId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       entityType: 'playlist' | ||||
|     } | ||||
|   }) | ||||
|   Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false }) | ||||
| 
 | ||||
|   Feed.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) { | ||||
|         instance.entity = instance.libraryItem | ||||
|         instance.dataValues.entity = instance.dataValues.libraryItem | ||||
|       } else if (instance.entityType === 'collection' && instance.collection !== undefined) { | ||||
|         instance.entity = instance.collection | ||||
|         instance.dataValues.entity = instance.dataValues.collection | ||||
|       } else if (instance.entityType === 'series' && instance.series !== undefined) { | ||||
|         instance.entity = instance.series | ||||
|         instance.dataValues.entity = instance.dataValues.series | ||||
|       } else if (instance.entityType === 'playlist' && instance.playlist !== undefined) { | ||||
|         instance.entity = instance.playlist | ||||
|         instance.dataValues.entity = instance.dataValues.playlist | ||||
|       } | ||||
| 
 | ||||
|       // To prevent mistakes:
 | ||||
|       delete instance.libraryItem | ||||
|       delete instance.dataValues.libraryItem | ||||
|       delete instance.collection | ||||
|       delete instance.dataValues.collection | ||||
|       delete instance.series | ||||
|       delete instance.dataValues.series | ||||
|       delete instance.playlist | ||||
|       delete instance.dataValues.playlist | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   return Feed | ||||
| } | ||||
							
								
								
									
										60
									
								
								server/models/FeedEpisode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								server/models/FeedEpisode.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class FeedEpisode extends Model { | ||||
|     getOldEpisode() { | ||||
|       const enclosure = { | ||||
|         url: this.enclosureURL, | ||||
|         size: this.enclosureSize, | ||||
|         type: this.enclosureType | ||||
|       } | ||||
|       return { | ||||
|         id: this.id, | ||||
|         title: this.title, | ||||
|         description: this.description, | ||||
|         enclosure, | ||||
|         pubDate: this.pubDate, | ||||
|         link: this.siteURL, | ||||
|         author: this.author, | ||||
|         explicit: this.explicit, | ||||
|         duration: this.duration, | ||||
|         season: this.season, | ||||
|         episode: this.episode, | ||||
|         episodeType: this.episodeType, | ||||
|         fullPath: this.filePath | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   FeedEpisode.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     title: DataTypes.STRING, | ||||
|     author: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT, | ||||
|     siteURL: DataTypes.STRING, | ||||
|     enclosureURL: DataTypes.STRING, | ||||
|     enclosureType: DataTypes.STRING, | ||||
|     enclosureSize: DataTypes.BIGINT, | ||||
|     pubDate: DataTypes.STRING, | ||||
|     season: DataTypes.STRING, | ||||
|     episode: DataTypes.STRING, | ||||
|     episodeType: DataTypes.STRING, | ||||
|     duration: DataTypes.FLOAT, | ||||
|     filePath: DataTypes.STRING, | ||||
|     explicit: DataTypes.BOOLEAN | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'feedEpisode' | ||||
|   }) | ||||
| 
 | ||||
|   const { feed } = sequelize.models | ||||
| 
 | ||||
|   feed.hasMany(FeedEpisode) | ||||
|   FeedEpisode.belongsTo(feed) | ||||
| 
 | ||||
|   return FeedEpisode | ||||
| } | ||||
							
								
								
									
										137
									
								
								server/models/Library.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								server/models/Library.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,137 @@ | ||||
| const Logger = require('../Logger') | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldLibrary = require('../objects/Library') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Library extends Model { | ||||
|     static async getAllOldLibraries() { | ||||
|       const libraries = await this.findAll({ | ||||
|         include: sequelize.models.libraryFolder | ||||
|       }) | ||||
|       return libraries.map(lib => this.getOldLibrary(lib)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldLibrary(libraryExpanded) { | ||||
|       const folders = libraryExpanded.libraryFolders.map(folder => { | ||||
|         return { | ||||
|           id: folder.id, | ||||
|           fullPath: folder.path, | ||||
|           libraryId: folder.libraryId, | ||||
|           addedAt: folder.createdAt.valueOf() | ||||
|         } | ||||
|       }) | ||||
|       return new oldLibrary({ | ||||
|         id: libraryExpanded.id, | ||||
|         name: libraryExpanded.name, | ||||
|         folders, | ||||
|         displayOrder: libraryExpanded.displayOrder, | ||||
|         icon: libraryExpanded.icon, | ||||
|         mediaType: libraryExpanded.mediaType, | ||||
|         provider: libraryExpanded.provider, | ||||
|         settings: libraryExpanded.settings, | ||||
|         createdAt: libraryExpanded.createdAt.valueOf(), | ||||
|         lastUpdate: libraryExpanded.updatedAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {object} oldLibrary  | ||||
|      * @returns {Library|null} | ||||
|      */ | ||||
|     static createFromOld(oldLibrary) { | ||||
|       const library = this.getFromOld(oldLibrary) | ||||
| 
 | ||||
|       library.libraryFolders = oldLibrary.folders.map(folder => { | ||||
|         return { | ||||
|           id: folder.id, | ||||
|           path: folder.fullPath, | ||||
|           libraryId: library.id | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       return this.create(library).catch((error) => { | ||||
|         Logger.error(`[Library] Failed to create library ${library.id}`, error) | ||||
|         return null | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static async updateFromOld(oldLibrary) { | ||||
|       const existingLibrary = await this.findByPk(oldLibrary.id, { | ||||
|         include: sequelize.models.libraryFolder | ||||
|       }) | ||||
|       if (!existingLibrary) { | ||||
|         Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`) | ||||
|         return null | ||||
|       } | ||||
| 
 | ||||
|       const library = this.getFromOld(oldLibrary) | ||||
| 
 | ||||
|       const libraryFolders = oldLibrary.folders.map(folder => { | ||||
|         return { | ||||
|           id: folder.id, | ||||
|           path: folder.fullPath, | ||||
|           libraryId: library.id | ||||
|         } | ||||
|       }) | ||||
|       for (const libraryFolder of libraryFolders) { | ||||
|         const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id) | ||||
|         if (!existingLibraryFolder) { | ||||
|           await sequelize.models.libraryFolder.create(libraryFolder) | ||||
|         } else if (existingLibraryFolder.path !== libraryFolder.path) { | ||||
|           await existingLibraryFolder.update({ path: libraryFolder.path }) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id)) | ||||
|       for (const existingLibraryFolder of libraryFoldersRemoved) { | ||||
|         await existingLibraryFolder.destroy() | ||||
|       } | ||||
| 
 | ||||
|       return existingLibrary.update(library) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldLibrary) { | ||||
|       return { | ||||
|         id: oldLibrary.id, | ||||
|         name: oldLibrary.name, | ||||
|         displayOrder: oldLibrary.displayOrder, | ||||
|         icon: oldLibrary.icon || null, | ||||
|         mediaType: oldLibrary.mediaType || null, | ||||
|         provider: oldLibrary.provider, | ||||
|         settings: oldLibrary.settings?.toJSON() || {}, | ||||
|         createdAt: oldLibrary.createdAt, | ||||
|         updatedAt: oldLibrary.lastUpdate | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(libraryId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: libraryId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Library.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     name: DataTypes.STRING, | ||||
|     displayOrder: DataTypes.INTEGER, | ||||
|     icon: DataTypes.STRING, | ||||
|     mediaType: DataTypes.STRING, | ||||
|     provider: DataTypes.STRING, | ||||
|     lastScan: DataTypes.DATE, | ||||
|     lastScanVersion: DataTypes.STRING, | ||||
|     settings: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'library' | ||||
|   }) | ||||
| 
 | ||||
|   return Library | ||||
| } | ||||
							
								
								
									
										23
									
								
								server/models/LibraryFolder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								server/models/LibraryFolder.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class LibraryFolder extends Model { } | ||||
| 
 | ||||
|   LibraryFolder.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     path: DataTypes.STRING | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'libraryFolder' | ||||
|   }) | ||||
| 
 | ||||
|   const { library } = sequelize.models | ||||
|   library.hasMany(LibraryFolder) | ||||
|   LibraryFolder.belongsTo(library) | ||||
| 
 | ||||
|   return LibraryFolder | ||||
| } | ||||
							
								
								
									
										393
									
								
								server/models/LibraryItem.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								server/models/LibraryItem.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,393 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| const Logger = require('../Logger') | ||||
| const oldLibraryItem = require('../objects/LibraryItem') | ||||
| const { areEquivalent } = require('../utils/index') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class LibraryItem extends Model { | ||||
|     static async getAllOldLibraryItems() { | ||||
|       let libraryItems = await this.findAll({ | ||||
|         include: [ | ||||
|           { | ||||
|             model: sequelize.models.book, | ||||
|             include: [ | ||||
|               { | ||||
|                 model: sequelize.models.author, | ||||
|                 through: { | ||||
|                   attributes: [] | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 model: sequelize.models.series, | ||||
|                 through: { | ||||
|                   attributes: ['sequence'] | ||||
|                 } | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           { | ||||
|             model: sequelize.models.podcast, | ||||
|             include: [ | ||||
|               { | ||||
|                 model: sequelize.models.podcastEpisode | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       return libraryItems.map(ti => this.getOldLibraryItem(ti)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldLibraryItem(libraryItemExpanded) { | ||||
|       let media = null | ||||
|       if (libraryItemExpanded.mediaType === 'book') { | ||||
|         media = sequelize.models.book.getOldBook(libraryItemExpanded) | ||||
|       } else if (libraryItemExpanded.mediaType === 'podcast') { | ||||
|         media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded) | ||||
|       } | ||||
| 
 | ||||
|       return new oldLibraryItem({ | ||||
|         id: libraryItemExpanded.id, | ||||
|         ino: libraryItemExpanded.ino, | ||||
|         libraryId: libraryItemExpanded.libraryId, | ||||
|         folderId: libraryItemExpanded.libraryFolderId, | ||||
|         path: libraryItemExpanded.path, | ||||
|         relPath: libraryItemExpanded.relPath, | ||||
|         isFile: libraryItemExpanded.isFile, | ||||
|         mtimeMs: libraryItemExpanded.mtime?.valueOf(), | ||||
|         ctimeMs: libraryItemExpanded.ctime?.valueOf(), | ||||
|         birthtimeMs: libraryItemExpanded.birthtime?.valueOf(), | ||||
|         addedAt: libraryItemExpanded.createdAt.valueOf(), | ||||
|         updatedAt: libraryItemExpanded.updatedAt.valueOf(), | ||||
|         lastScan: libraryItemExpanded.lastScan?.valueOf(), | ||||
|         scanVersion: libraryItemExpanded.lastScanVersion, | ||||
|         isMissing: !!libraryItemExpanded.isMissing, | ||||
|         isInvalid: !!libraryItemExpanded.isInvalid, | ||||
|         mediaType: libraryItemExpanded.mediaType, | ||||
|         media, | ||||
|         libraryFiles: libraryItemExpanded.libraryFiles | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static async fullCreateFromOld(oldLibraryItem) { | ||||
|       const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem)) | ||||
| 
 | ||||
|       if (oldLibraryItem.mediaType === 'book') { | ||||
|         const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media) | ||||
|         bookObj.libraryItemId = newLibraryItem.id | ||||
|         const newBook = await sequelize.models.book.create(bookObj) | ||||
| 
 | ||||
|         const oldBookAuthors = oldLibraryItem.media.metadata.authors || [] | ||||
|         const oldBookSeriesAll = oldLibraryItem.media.metadata.series || [] | ||||
| 
 | ||||
|         for (const oldBookAuthor of oldBookAuthors) { | ||||
|           await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id }) | ||||
|         } | ||||
|         for (const oldSeries of oldBookSeriesAll) { | ||||
|           await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence }) | ||||
|         } | ||||
|       } else if (oldLibraryItem.mediaType === 'podcast') { | ||||
|         const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media) | ||||
|         podcastObj.libraryItemId = newLibraryItem.id | ||||
|         const newPodcast = await sequelize.models.podcast.create(podcastObj) | ||||
| 
 | ||||
|         const oldEpisodes = oldLibraryItem.media.episodes || [] | ||||
|         for (const oldEpisode of oldEpisodes) { | ||||
|           const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode) | ||||
|           episodeObj.libraryItemId = newLibraryItem.id | ||||
|           episodeObj.podcastId = newPodcast.id | ||||
|           await sequelize.models.podcastEpisode.create(episodeObj) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return newLibraryItem | ||||
|     } | ||||
| 
 | ||||
|     static async fullUpdateFromOld(oldLibraryItem) { | ||||
|       const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { | ||||
|         include: [ | ||||
|           { | ||||
|             model: sequelize.models.book, | ||||
|             include: [ | ||||
|               { | ||||
|                 model: sequelize.models.author, | ||||
|                 through: { | ||||
|                   attributes: [] | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 model: sequelize.models.series, | ||||
|                 through: { | ||||
|                   attributes: ['sequence'] | ||||
|                 } | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           { | ||||
|             model: sequelize.models.podcast, | ||||
|             include: [ | ||||
|               { | ||||
|                 model: sequelize.models.podcastEpisode | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       if (!libraryItemExpanded) return false | ||||
| 
 | ||||
|       let hasUpdates = false | ||||
| 
 | ||||
|       // Check update Book/Podcast
 | ||||
|       if (libraryItemExpanded.media) { | ||||
|         let updatedMedia = null | ||||
|         if (libraryItemExpanded.mediaType === 'podcast') { | ||||
|           updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media) | ||||
| 
 | ||||
|           const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || [] | ||||
|           const updatedPodcastEpisodes = oldLibraryItem.media.episodes || [] | ||||
| 
 | ||||
|           for (const existingPodcastEpisode of existingPodcastEpisodes) { | ||||
|             // Episode was removed
 | ||||
|             if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) | ||||
|               await existingPodcastEpisode.destroy() | ||||
|               hasUpdates = true | ||||
|             } | ||||
|           } | ||||
|           for (const updatedPodcastEpisode of updatedPodcastEpisodes) { | ||||
|             const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) | ||||
|             if (!existingEpisodeMatch) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) | ||||
|               await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) | ||||
|               hasUpdates = true | ||||
|             } else { | ||||
|               const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode) | ||||
|               let episodeHasUpdates = false | ||||
|               for (const key in updatedEpisodeCleaned) { | ||||
|                 let existingValue = existingEpisodeMatch[key] | ||||
|                 if (existingValue instanceof Date) existingValue = existingValue.valueOf() | ||||
| 
 | ||||
|                 if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) { | ||||
|                   Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`) | ||||
|                   episodeHasUpdates = true | ||||
|                 } | ||||
|               } | ||||
|               if (episodeHasUpdates) { | ||||
|                 await existingEpisodeMatch.update(updatedEpisodeCleaned) | ||||
|                 hasUpdates = true | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } else if (libraryItemExpanded.mediaType === 'book') { | ||||
|           updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media) | ||||
| 
 | ||||
|           const existingAuthors = libraryItemExpanded.media.authors || [] | ||||
|           const existingSeriesAll = libraryItemExpanded.media.series || [] | ||||
|           const updatedAuthors = oldLibraryItem.media.metadata.authors || [] | ||||
|           const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] | ||||
| 
 | ||||
|           for (const existingAuthor of existingAuthors) { | ||||
|             // Author was removed from Book
 | ||||
|             if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) | ||||
|               await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) | ||||
|               hasUpdates = true | ||||
|             } | ||||
|           } | ||||
|           for (const updatedAuthor of updatedAuthors) { | ||||
|             // Author was added
 | ||||
|             if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) | ||||
|               await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) | ||||
|               hasUpdates = true | ||||
|             } | ||||
|           } | ||||
|           for (const existingSeries of existingSeriesAll) { | ||||
|             // Series was removed
 | ||||
|             if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) | ||||
|               await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) | ||||
|               hasUpdates = true | ||||
|             } | ||||
|           } | ||||
|           for (const updatedSeries of updatedSeriesAll) { | ||||
|             // Series was added/updated
 | ||||
|             const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) | ||||
|             if (!existingSeriesMatch) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) | ||||
|               await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) | ||||
|               hasUpdates = true | ||||
|             } else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) { | ||||
|               Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`) | ||||
|               await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence }) | ||||
|               hasUpdates = true | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         let hasMediaUpdates = false | ||||
|         for (const key in updatedMedia) { | ||||
|           let existingValue = libraryItemExpanded.media[key] | ||||
|           if (existingValue instanceof Date) existingValue = existingValue.valueOf() | ||||
| 
 | ||||
|           if (!areEquivalent(updatedMedia[key], existingValue, true)) { | ||||
|             Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) | ||||
|             hasMediaUpdates = true | ||||
|           } | ||||
|         } | ||||
|         if (hasMediaUpdates && updatedMedia) { | ||||
|           await libraryItemExpanded.media.update(updatedMedia) | ||||
|           hasUpdates = true | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const updatedLibraryItem = this.getFromOld(oldLibraryItem) | ||||
|       let hasLibraryItemUpdates = false | ||||
|       for (const key in updatedLibraryItem) { | ||||
|         let existingValue = libraryItemExpanded[key] | ||||
|         if (existingValue instanceof Date) existingValue = existingValue.valueOf() | ||||
| 
 | ||||
|         if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { | ||||
|           Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) | ||||
|           hasLibraryItemUpdates = true | ||||
|         } | ||||
|       } | ||||
|       if (hasLibraryItemUpdates) { | ||||
|         await libraryItemExpanded.update(updatedLibraryItem) | ||||
|         hasUpdates = true | ||||
|       } | ||||
|       return hasUpdates | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldLibraryItem) { | ||||
|       const libraryItem = this.getFromOld(oldLibraryItem) | ||||
|       return this.update(libraryItem, { | ||||
|         where: { | ||||
|           id: libraryItem.id | ||||
|         } | ||||
|       }).then((result) => result[0] > 0).catch((error) => { | ||||
|         Logger.error(`[LibraryItem] Failed to update libraryItem ${libraryItem.id}`, error) | ||||
|         return false | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldLibraryItem) { | ||||
|       return { | ||||
|         id: oldLibraryItem.id, | ||||
|         ino: oldLibraryItem.ino, | ||||
|         path: oldLibraryItem.path, | ||||
|         relPath: oldLibraryItem.relPath, | ||||
|         mediaId: oldLibraryItem.media.id, | ||||
|         mediaType: oldLibraryItem.mediaType, | ||||
|         isFile: !!oldLibraryItem.isFile, | ||||
|         isMissing: !!oldLibraryItem.isMissing, | ||||
|         isInvalid: !!oldLibraryItem.isInvalid, | ||||
|         mtime: oldLibraryItem.mtimeMs, | ||||
|         ctime: oldLibraryItem.ctimeMs, | ||||
|         birthtime: oldLibraryItem.birthtimeMs, | ||||
|         lastScan: oldLibraryItem.lastScan, | ||||
|         lastScanVersion: oldLibraryItem.scanVersion, | ||||
|         createdAt: oldLibraryItem.addedAt, | ||||
|         updatedAt: oldLibraryItem.updatedAt, | ||||
|         libraryId: oldLibraryItem.libraryId, | ||||
|         libraryFolderId: oldLibraryItem.folderId, | ||||
|         libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [] | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(libraryItemId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: libraryItemId | ||||
|         }, | ||||
|         individualHooks: true | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     getMedia(options) { | ||||
|       if (!this.mediaType) return Promise.resolve(null) | ||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}` | ||||
|       return this[mixinMethodName](options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   LibraryItem.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     ino: DataTypes.STRING, | ||||
|     path: DataTypes.STRING, | ||||
|     relPath: DataTypes.STRING, | ||||
|     mediaId: DataTypes.UUIDV4, | ||||
|     mediaType: DataTypes.STRING, | ||||
|     isFile: DataTypes.BOOLEAN, | ||||
|     isMissing: DataTypes.BOOLEAN, | ||||
|     isInvalid: DataTypes.BOOLEAN, | ||||
|     mtime: DataTypes.DATE(6), | ||||
|     ctime: DataTypes.DATE(6), | ||||
|     birthtime: DataTypes.DATE(6), | ||||
|     lastScan: DataTypes.DATE, | ||||
|     lastScanVersion: DataTypes.STRING, | ||||
|     libraryFiles: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'libraryItem' | ||||
|   }) | ||||
| 
 | ||||
|   const { library, libraryFolder, book, podcast } = sequelize.models | ||||
|   library.hasMany(LibraryItem) | ||||
|   LibraryItem.belongsTo(library) | ||||
| 
 | ||||
|   libraryFolder.hasMany(LibraryItem) | ||||
|   LibraryItem.belongsTo(libraryFolder) | ||||
| 
 | ||||
|   book.hasOne(LibraryItem, { | ||||
|     foreignKey: 'mediaId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaType: 'book' | ||||
|     } | ||||
|   }) | ||||
|   LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false }) | ||||
| 
 | ||||
|   podcast.hasOne(LibraryItem, { | ||||
|     foreignKey: 'mediaId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaType: 'podcast' | ||||
|     } | ||||
|   }) | ||||
|   LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false }) | ||||
| 
 | ||||
|   LibraryItem.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.mediaType === 'book' && instance.book !== undefined) { | ||||
|         instance.media = instance.book | ||||
|         instance.dataValues.media = instance.dataValues.book | ||||
|       } else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) { | ||||
|         instance.media = instance.podcast | ||||
|         instance.dataValues.media = instance.dataValues.podcast | ||||
|       } | ||||
|       // To prevent mistakes:
 | ||||
|       delete instance.book | ||||
|       delete instance.dataValues.book | ||||
|       delete instance.podcast | ||||
|       delete instance.dataValues.podcast | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   LibraryItem.addHook('afterDestroy', async instance => { | ||||
|     if (!instance) return | ||||
|     const media = await instance.getMedia() | ||||
|     if (media) { | ||||
|       media.destroy() | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   return LibraryItem | ||||
| } | ||||
							
								
								
									
										141
									
								
								server/models/MediaProgress.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								server/models/MediaProgress.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| /* | ||||
|  * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
 | ||||
|  * Book has many MediaProgress. PodcastEpisode has many MediaProgress. | ||||
|  */ | ||||
| module.exports = (sequelize) => { | ||||
|   class MediaProgress extends Model { | ||||
|     getOldMediaProgress() { | ||||
|       const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' | ||||
| 
 | ||||
|       return { | ||||
|         id: this.id, | ||||
|         userId: this.userId, | ||||
|         libraryItemId: this.extraData?.libraryItemId || null, | ||||
|         episodeId: isPodcastEpisode ? this.mediaItemId : null, | ||||
|         mediaItemId: this.mediaItemId, | ||||
|         mediaItemType: this.mediaItemType, | ||||
|         duration: this.duration, | ||||
|         progress: this.extraData?.progress || null, | ||||
|         currentTime: this.currentTime, | ||||
|         isFinished: !!this.isFinished, | ||||
|         hideFromContinueListening: !!this.hideFromContinueListening, | ||||
|         ebookLocation: this.ebookLocation, | ||||
|         ebookProgress: this.ebookProgress, | ||||
|         lastUpdate: this.updatedAt.valueOf(), | ||||
|         startedAt: this.createdAt.valueOf(), | ||||
|         finishedAt: this.finishedAt?.valueOf() || null | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static upsertFromOld(oldMediaProgress) { | ||||
|       const mediaProgress = this.getFromOld(oldMediaProgress) | ||||
|       return this.upsert(mediaProgress) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldMediaProgress) { | ||||
|       return { | ||||
|         id: oldMediaProgress.id, | ||||
|         userId: oldMediaProgress.userId, | ||||
|         mediaItemId: oldMediaProgress.mediaItemId, | ||||
|         mediaItemType: oldMediaProgress.mediaItemType, | ||||
|         duration: oldMediaProgress.duration, | ||||
|         currentTime: oldMediaProgress.currentTime, | ||||
|         ebookLocation: oldMediaProgress.ebookLocation || null, | ||||
|         ebookProgress: oldMediaProgress.ebookProgress || null, | ||||
|         isFinished: !!oldMediaProgress.isFinished, | ||||
|         hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, | ||||
|         finishedAt: oldMediaProgress.finishedAt, | ||||
|         createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, | ||||
|         updatedAt: oldMediaProgress.lastUpdate, | ||||
|         extraData: { | ||||
|           libraryItemId: oldMediaProgress.libraryItemId, | ||||
|           progress: oldMediaProgress.progress | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(mediaProgressId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: mediaProgressId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     getMediaItem(options) { | ||||
|       if (!this.mediaItemType) return Promise.resolve(null) | ||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` | ||||
|       return this[mixinMethodName](options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   MediaProgress.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     mediaItemId: DataTypes.UUIDV4, | ||||
|     mediaItemType: DataTypes.STRING, | ||||
|     duration: DataTypes.FLOAT, | ||||
|     currentTime: DataTypes.FLOAT, | ||||
|     isFinished: DataTypes.BOOLEAN, | ||||
|     hideFromContinueListening: DataTypes.BOOLEAN, | ||||
|     ebookLocation: DataTypes.STRING, | ||||
|     ebookProgress: DataTypes.FLOAT, | ||||
|     finishedAt: DataTypes.DATE, | ||||
|     extraData: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'mediaProgress' | ||||
|   }) | ||||
| 
 | ||||
|   const { book, podcastEpisode, user } = sequelize.models | ||||
| 
 | ||||
|   book.hasMany(MediaProgress, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'book' | ||||
|     } | ||||
|   }) | ||||
|   MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   podcastEpisode.hasMany(MediaProgress, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'podcastEpisode' | ||||
|     } | ||||
|   }) | ||||
|   MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   MediaProgress.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
| 
 | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||
|         instance.mediaItem = instance.book | ||||
|         instance.dataValues.mediaItem = instance.dataValues.book | ||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||
|         instance.mediaItem = instance.podcastEpisode | ||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||
|       } | ||||
|       // To prevent mistakes:
 | ||||
|       delete instance.book | ||||
|       delete instance.dataValues.book | ||||
|       delete instance.podcastEpisode | ||||
|       delete instance.dataValues.podcastEpisode | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   user.hasMany(MediaProgress) | ||||
|   MediaProgress.belongsTo(user) | ||||
| 
 | ||||
|   return MediaProgress | ||||
| } | ||||
							
								
								
									
										198
									
								
								server/models/PlaybackSession.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								server/models/PlaybackSession.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,198 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldPlaybackSession = require('../objects/PlaybackSession') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class PlaybackSession extends Model { | ||||
|     static async getOldPlaybackSessions(where = null) { | ||||
|       const playbackSessions = await this.findAll({ | ||||
|         where, | ||||
|         include: [ | ||||
|           { | ||||
|             model: sequelize.models.device | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       return playbackSessions.map(session => this.getOldPlaybackSession(session)) | ||||
|     } | ||||
| 
 | ||||
|     static async getById(sessionId) { | ||||
|       const playbackSession = await this.findByPk(sessionId, { | ||||
|         include: [ | ||||
|           { | ||||
|             model: sequelize.models.device | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       if (!playbackSession) return null | ||||
|       return this.getOldPlaybackSession(playbackSession) | ||||
|     } | ||||
| 
 | ||||
|     static getOldPlaybackSession(playbackSessionExpanded) { | ||||
|       const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode' | ||||
| 
 | ||||
|       return new oldPlaybackSession({ | ||||
|         id: playbackSessionExpanded.id, | ||||
|         userId: playbackSessionExpanded.userId, | ||||
|         libraryId: playbackSessionExpanded.libraryId, | ||||
|         libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null, | ||||
|         bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId, | ||||
|         episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null, | ||||
|         mediaType: isPodcastEpisode ? 'podcast' : 'book', | ||||
|         mediaMetadata: playbackSessionExpanded.mediaMetadata, | ||||
|         chapters: null, | ||||
|         displayTitle: playbackSessionExpanded.displayTitle, | ||||
|         displayAuthor: playbackSessionExpanded.displayAuthor, | ||||
|         coverPath: playbackSessionExpanded.coverPath, | ||||
|         duration: playbackSessionExpanded.duration, | ||||
|         playMethod: playbackSessionExpanded.playMethod, | ||||
|         mediaPlayer: playbackSessionExpanded.mediaPlayer, | ||||
|         deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null, | ||||
|         serverVersion: playbackSessionExpanded.serverVersion, | ||||
|         date: playbackSessionExpanded.date, | ||||
|         dayOfWeek: playbackSessionExpanded.dayOfWeek, | ||||
|         timeListening: playbackSessionExpanded.timeListening, | ||||
|         startTime: playbackSessionExpanded.startTime, | ||||
|         currentTime: playbackSessionExpanded.currentTime, | ||||
|         startedAt: playbackSessionExpanded.createdAt.valueOf(), | ||||
|         updatedAt: playbackSessionExpanded.updatedAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static removeById(sessionId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: sessionId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldPlaybackSession) { | ||||
|       const playbackSession = this.getFromOld(oldPlaybackSession) | ||||
|       return this.create(playbackSession) | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldPlaybackSession) { | ||||
|       const playbackSession = this.getFromOld(oldPlaybackSession) | ||||
|       return this.update(playbackSession, { | ||||
|         where: { | ||||
|           id: playbackSession.id | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldPlaybackSession) { | ||||
|       return { | ||||
|         id: oldPlaybackSession.id, | ||||
|         mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId, | ||||
|         mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book', | ||||
|         libraryId: oldPlaybackSession.libraryId, | ||||
|         displayTitle: oldPlaybackSession.displayTitle, | ||||
|         displayAuthor: oldPlaybackSession.displayAuthor, | ||||
|         duration: oldPlaybackSession.duration, | ||||
|         playMethod: oldPlaybackSession.playMethod, | ||||
|         mediaPlayer: oldPlaybackSession.mediaPlayer, | ||||
|         startTime: oldPlaybackSession.startTime, | ||||
|         currentTime: oldPlaybackSession.currentTime, | ||||
|         serverVersion: oldPlaybackSession.serverVersion || null, | ||||
|         createdAt: oldPlaybackSession.startedAt, | ||||
|         updatedAt: oldPlaybackSession.updatedAt, | ||||
|         userId: oldPlaybackSession.userId, | ||||
|         deviceId: oldPlaybackSession.deviceInfo?.id || null, | ||||
|         timeListening: oldPlaybackSession.timeListening, | ||||
|         coverPath: oldPlaybackSession.coverPath, | ||||
|         mediaMetadata: oldPlaybackSession.mediaMetadata, | ||||
|         date: oldPlaybackSession.date, | ||||
|         dayOfWeek: oldPlaybackSession.dayOfWeek, | ||||
|         extraData: { | ||||
|           libraryItemId: oldPlaybackSession.libraryItemId | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     getMediaItem(options) { | ||||
|       if (!this.mediaItemType) return Promise.resolve(null) | ||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` | ||||
|       return this[mixinMethodName](options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   PlaybackSession.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     mediaItemId: DataTypes.UUIDV4, | ||||
|     mediaItemType: DataTypes.STRING, | ||||
|     displayTitle: DataTypes.STRING, | ||||
|     displayAuthor: DataTypes.STRING, | ||||
|     duration: DataTypes.FLOAT, | ||||
|     playMethod: DataTypes.INTEGER, | ||||
|     mediaPlayer: DataTypes.STRING, | ||||
|     startTime: DataTypes.FLOAT, | ||||
|     currentTime: DataTypes.FLOAT, | ||||
|     serverVersion: DataTypes.STRING, | ||||
|     coverPath: DataTypes.STRING, | ||||
|     timeListening: DataTypes.INTEGER, | ||||
|     mediaMetadata: DataTypes.JSON, | ||||
|     date: DataTypes.STRING, | ||||
|     dayOfWeek: DataTypes.STRING, | ||||
|     extraData: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'playbackSession' | ||||
|   }) | ||||
| 
 | ||||
|   const { book, podcastEpisode, user, device, library } = sequelize.models | ||||
| 
 | ||||
|   user.hasMany(PlaybackSession) | ||||
|   PlaybackSession.belongsTo(user) | ||||
| 
 | ||||
|   device.hasMany(PlaybackSession) | ||||
|   PlaybackSession.belongsTo(device) | ||||
| 
 | ||||
|   library.hasMany(PlaybackSession) | ||||
|   PlaybackSession.belongsTo(library) | ||||
| 
 | ||||
|   book.hasMany(PlaybackSession, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'book' | ||||
|     } | ||||
|   }) | ||||
|   PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   podcastEpisode.hasOne(PlaybackSession, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'podcastEpisode' | ||||
|     } | ||||
|   }) | ||||
|   PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   PlaybackSession.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
| 
 | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||
|         instance.mediaItem = instance.book | ||||
|         instance.dataValues.mediaItem = instance.dataValues.book | ||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||
|         instance.mediaItem = instance.podcastEpisode | ||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||
|       } | ||||
|       // To prevent mistakes:
 | ||||
|       delete instance.book | ||||
|       delete instance.dataValues.book | ||||
|       delete instance.podcastEpisode | ||||
|       delete instance.dataValues.podcastEpisode | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   return PlaybackSession | ||||
| } | ||||
							
								
								
									
										171
									
								
								server/models/Playlist.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								server/models/Playlist.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,171 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldPlaylist = require('../objects/Playlist') | ||||
| const { areEquivalent } = require('../utils/index') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Playlist extends Model { | ||||
|     static async getOldPlaylists() { | ||||
|       const playlists = await this.findAll({ | ||||
|         include: { | ||||
|           model: sequelize.models.playlistMediaItem, | ||||
|           include: [ | ||||
|             { | ||||
|               model: sequelize.models.book, | ||||
|               include: sequelize.models.libraryItem | ||||
|             }, | ||||
|             { | ||||
|               model: sequelize.models.podcastEpisode, | ||||
|               include: { | ||||
|                 model: sequelize.models.podcast, | ||||
|                 include: sequelize.models.libraryItem | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       }) | ||||
|       return playlists.map(p => this.getOldPlaylist(p)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldPlaylist(playlistExpanded) { | ||||
|       const items = playlistExpanded.playlistMediaItems.map(pmi => { | ||||
|         const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null | ||||
|         if (!libraryItemId) { | ||||
|           console.log(JSON.stringify(pmi, null, 2)) | ||||
|           throw new Error('No library item id') | ||||
|         } | ||||
|         return { | ||||
|           episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '', | ||||
|           libraryItemId: libraryItemId | ||||
|         } | ||||
|       }) | ||||
|       return new oldPlaylist({ | ||||
|         id: playlistExpanded.id, | ||||
|         libraryId: playlistExpanded.libraryId, | ||||
|         userId: playlistExpanded.userId, | ||||
|         name: playlistExpanded.name, | ||||
|         description: playlistExpanded.description, | ||||
|         items, | ||||
|         lastUpdate: playlistExpanded.updatedAt.valueOf(), | ||||
|         createdAt: playlistExpanded.createdAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldPlaylist) { | ||||
|       const playlist = this.getFromOld(oldPlaylist) | ||||
|       return this.create(playlist) | ||||
|     } | ||||
| 
 | ||||
|     static async fullUpdateFromOld(oldPlaylist, playlistMediaItems) { | ||||
|       const existingPlaylist = await this.findByPk(oldPlaylist.id, { | ||||
|         include: sequelize.models.playlistMediaItem | ||||
|       }) | ||||
|       if (!existingPlaylist) return false | ||||
| 
 | ||||
|       let hasUpdates = false | ||||
|       const playlist = this.getFromOld(oldPlaylist) | ||||
| 
 | ||||
|       for (const pmi of playlistMediaItems) { | ||||
|         const existingPmi = existingPlaylist.playlistMediaItems.find(i => i.mediaItemId === pmi.mediaItemId) | ||||
|         if (!existingPmi) { | ||||
|           await sequelize.models.playlistMediaItem.create(pmi) | ||||
|           hasUpdates = true | ||||
|         } else if (existingPmi.order != pmi.order) { | ||||
|           await existingPmi.update({ order: pmi.order }) | ||||
|           hasUpdates = true | ||||
|         } | ||||
|       } | ||||
|       for (const pmi of existingPlaylist.playlistMediaItems) { | ||||
|         // Pmi was removed
 | ||||
|         if (!playlistMediaItems.some(i => i.mediaItemId === pmi.mediaItemId)) { | ||||
|           await pmi.destroy() | ||||
|           hasUpdates = true | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       let hasPlaylistUpdates = false | ||||
|       for (const key in playlist) { | ||||
|         let existingValue = existingPlaylist[key] | ||||
|         if (existingValue instanceof Date) existingValue = existingValue.valueOf() | ||||
| 
 | ||||
|         if (!areEquivalent(playlist[key], existingValue)) { | ||||
|           hasPlaylistUpdates = true | ||||
|         } | ||||
|       } | ||||
|       if (hasPlaylistUpdates) { | ||||
|         existingPlaylist.update(playlist) | ||||
|         hasUpdates = true | ||||
|       } | ||||
|       return hasUpdates | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldPlaylist) { | ||||
|       return { | ||||
|         id: oldPlaylist.id, | ||||
|         name: oldPlaylist.name, | ||||
|         description: oldPlaylist.description, | ||||
|         createdAt: oldPlaylist.createdAt, | ||||
|         updatedAt: oldPlaylist.lastUpdate, | ||||
|         userId: oldPlaylist.userId, | ||||
|         libraryId: oldPlaylist.libraryId | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(playlistId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: playlistId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Playlist.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     name: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'playlist' | ||||
|   }) | ||||
| 
 | ||||
|   const { library, user } = sequelize.models | ||||
|   library.hasMany(Playlist) | ||||
|   Playlist.belongsTo(library) | ||||
| 
 | ||||
|   user.hasMany(Playlist) | ||||
|   Playlist.belongsTo(user) | ||||
| 
 | ||||
|   Playlist.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
| 
 | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.playlistMediaItems?.length) { | ||||
|         instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => { | ||||
|           if (pmi.mediaItemType === 'book' && pmi.book !== undefined) { | ||||
|             pmi.mediaItem = pmi.book | ||||
|             pmi.dataValues.mediaItem = pmi.dataValues.book | ||||
|           } else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) { | ||||
|             pmi.mediaItem = pmi.podcastEpisode | ||||
|             pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode | ||||
|           } | ||||
|           // To prevent mistakes:
 | ||||
|           delete pmi.book | ||||
|           delete pmi.dataValues.book | ||||
|           delete pmi.podcastEpisode | ||||
|           delete pmi.dataValues.podcastEpisode | ||||
|           return pmi | ||||
|         }) | ||||
|       } | ||||
| 
 | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   return Playlist | ||||
| } | ||||
							
								
								
									
										82
									
								
								server/models/PlaylistMediaItem.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								server/models/PlaylistMediaItem.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class PlaylistMediaItem extends Model { | ||||
|     static removeByIds(playlistId, mediaItemId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           playlistId, | ||||
|           mediaItemId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     getMediaItem(options) { | ||||
|       if (!this.mediaItemType) return Promise.resolve(null) | ||||
|       const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}` | ||||
|       return this[mixinMethodName](options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   PlaylistMediaItem.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     mediaItemId: DataTypes.UUIDV4, | ||||
|     mediaItemType: DataTypes.STRING, | ||||
|     order: DataTypes.INTEGER | ||||
|   }, { | ||||
|     sequelize, | ||||
|     timestamps: true, | ||||
|     updatedAt: false, | ||||
|     modelName: 'playlistMediaItem' | ||||
|   }) | ||||
| 
 | ||||
|   const { book, podcastEpisode, playlist } = sequelize.models | ||||
| 
 | ||||
|   book.hasMany(PlaylistMediaItem, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'book' | ||||
|     } | ||||
|   }) | ||||
|   PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   podcastEpisode.hasOne(PlaylistMediaItem, { | ||||
|     foreignKey: 'mediaItemId', | ||||
|     constraints: false, | ||||
|     scope: { | ||||
|       mediaItemType: 'podcastEpisode' | ||||
|     } | ||||
|   }) | ||||
|   PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) | ||||
| 
 | ||||
|   PlaylistMediaItem.addHook('afterFind', findResult => { | ||||
|     if (!findResult) return | ||||
| 
 | ||||
|     if (!Array.isArray(findResult)) findResult = [findResult] | ||||
| 
 | ||||
|     for (const instance of findResult) { | ||||
|       if (instance.mediaItemType === 'book' && instance.book !== undefined) { | ||||
|         instance.mediaItem = instance.book | ||||
|         instance.dataValues.mediaItem = instance.dataValues.book | ||||
|       } else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) { | ||||
|         instance.mediaItem = instance.podcastEpisode | ||||
|         instance.dataValues.mediaItem = instance.dataValues.podcastEpisode | ||||
|       } | ||||
|       // To prevent mistakes:
 | ||||
|       delete instance.book | ||||
|       delete instance.dataValues.book | ||||
|       delete instance.podcastEpisode | ||||
|       delete instance.dataValues.podcastEpisode | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   playlist.hasMany(PlaylistMediaItem) | ||||
|   PlaylistMediaItem.belongsTo(playlist) | ||||
| 
 | ||||
|   return PlaylistMediaItem | ||||
| } | ||||
							
								
								
									
										98
									
								
								server/models/Podcast.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								server/models/Podcast.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Podcast extends Model { | ||||
|     static getOldPodcast(libraryItemExpanded) { | ||||
|       const podcastExpanded = libraryItemExpanded.media | ||||
|       const podcastEpisodes = podcastExpanded.podcastEpisodes.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index) | ||||
|       return { | ||||
|         id: podcastExpanded.id, | ||||
|         libraryItemId: libraryItemExpanded.id, | ||||
|         metadata: { | ||||
|           title: podcastExpanded.title, | ||||
|           author: podcastExpanded.author, | ||||
|           description: podcastExpanded.description, | ||||
|           releaseDate: podcastExpanded.releaseDate, | ||||
|           genres: podcastExpanded.genres, | ||||
|           feedUrl: podcastExpanded.feedURL, | ||||
|           imageUrl: podcastExpanded.imageURL, | ||||
|           itunesPageUrl: podcastExpanded.itunesPageURL, | ||||
|           itunesId: podcastExpanded.itunesId, | ||||
|           itunesArtistId: podcastExpanded.itunesArtistId, | ||||
|           explicit: podcastExpanded.explicit, | ||||
|           language: podcastExpanded.language, | ||||
|           type: podcastExpanded.podcastType | ||||
|         }, | ||||
|         coverPath: podcastExpanded.coverPath, | ||||
|         tags: podcastExpanded.tags, | ||||
|         episodes: podcastEpisodes, | ||||
|         autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes, | ||||
|         autoDownloadSchedule: podcastExpanded.autoDownloadSchedule, | ||||
|         lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null, | ||||
|         maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep, | ||||
|         maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldPodcast) { | ||||
|       const oldPodcastMetadata = oldPodcast.metadata | ||||
|       return { | ||||
|         id: oldPodcast.id, | ||||
|         title: oldPodcastMetadata.title, | ||||
|         author: oldPodcastMetadata.author, | ||||
|         releaseDate: oldPodcastMetadata.releaseDate, | ||||
|         feedURL: oldPodcastMetadata.feedUrl, | ||||
|         imageURL: oldPodcastMetadata.imageUrl, | ||||
|         description: oldPodcastMetadata.description, | ||||
|         itunesPageURL: oldPodcastMetadata.itunesPageUrl, | ||||
|         itunesId: oldPodcastMetadata.itunesId, | ||||
|         itunesArtistId: oldPodcastMetadata.itunesArtistId, | ||||
|         language: oldPodcastMetadata.language, | ||||
|         podcastType: oldPodcastMetadata.type, | ||||
|         explicit: !!oldPodcastMetadata.explicit, | ||||
|         autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, | ||||
|         autoDownloadSchedule: oldPodcast.autoDownloadSchedule, | ||||
|         lastEpisodeCheck: oldPodcast.lastEpisodeCheck, | ||||
|         maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, | ||||
|         maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, | ||||
|         coverPath: oldPodcast.coverPath, | ||||
|         tags: oldPodcast.tags, | ||||
|         genres: oldPodcastMetadata.genres | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Podcast.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     title: DataTypes.STRING, | ||||
|     author: DataTypes.STRING, | ||||
|     releaseDate: DataTypes.STRING, | ||||
|     feedURL: DataTypes.STRING, | ||||
|     imageURL: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT, | ||||
|     itunesPageURL: DataTypes.STRING, | ||||
|     itunesId: DataTypes.STRING, | ||||
|     itunesArtistId: DataTypes.STRING, | ||||
|     language: DataTypes.STRING, | ||||
|     podcastType: DataTypes.STRING, | ||||
|     explicit: DataTypes.BOOLEAN, | ||||
| 
 | ||||
|     autoDownloadEpisodes: DataTypes.BOOLEAN, | ||||
|     autoDownloadSchedule: DataTypes.STRING, | ||||
|     lastEpisodeCheck: DataTypes.DATE, | ||||
|     maxEpisodesToKeep: DataTypes.INTEGER, | ||||
|     maxNewEpisodesToDownload: DataTypes.INTEGER, | ||||
|     coverPath: DataTypes.STRING, | ||||
|     tags: DataTypes.JSON, | ||||
|     genres: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'podcast' | ||||
|   }) | ||||
| 
 | ||||
|   return Podcast | ||||
| } | ||||
							
								
								
									
										95
									
								
								server/models/PodcastEpisode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								server/models/PodcastEpisode.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class PodcastEpisode extends Model { | ||||
|     getOldPodcastEpisode(libraryItemId = null) { | ||||
|       let enclosure = null | ||||
|       if (this.enclosureURL) { | ||||
|         enclosure = { | ||||
|           url: this.enclosureURL, | ||||
|           type: this.enclosureType, | ||||
|           length: this.enclosureSize !== null ? String(this.enclosureSize) : null | ||||
|         } | ||||
|       } | ||||
|       return { | ||||
|         libraryItemId: libraryItemId || null, | ||||
|         podcastId: this.podcastId, | ||||
|         id: this.id, | ||||
|         index: this.index, | ||||
|         season: this.season, | ||||
|         episode: this.episode, | ||||
|         episodeType: this.episodeType, | ||||
|         title: this.title, | ||||
|         subtitle: this.subtitle, | ||||
|         description: this.description, | ||||
|         enclosure, | ||||
|         pubDate: this.pubDate, | ||||
|         chapters: this.chapters, | ||||
|         audioFile: this.audioFile, | ||||
|         publishedAt: this.publishedAt?.valueOf() || null, | ||||
|         addedAt: this.createdAt.valueOf(), | ||||
|         updatedAt: this.updatedAt.valueOf() | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldEpisode) { | ||||
|       const podcastEpisode = this.getFromOld(oldEpisode) | ||||
|       return this.create(podcastEpisode) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldEpisode) { | ||||
|       return { | ||||
|         id: oldEpisode.id, | ||||
|         index: oldEpisode.index, | ||||
|         season: oldEpisode.season, | ||||
|         episode: oldEpisode.episode, | ||||
|         episodeType: oldEpisode.episodeType, | ||||
|         title: oldEpisode.title, | ||||
|         subtitle: oldEpisode.subtitle, | ||||
|         description: oldEpisode.description, | ||||
|         pubDate: oldEpisode.pubDate, | ||||
|         enclosureURL: oldEpisode.enclosure?.url || null, | ||||
|         enclosureSize: oldEpisode.enclosure?.length || null, | ||||
|         enclosureType: oldEpisode.enclosure?.type || null, | ||||
|         publishedAt: oldEpisode.publishedAt, | ||||
|         createdAt: oldEpisode.addedAt, | ||||
|         updatedAt: oldEpisode.updatedAt, | ||||
|         podcastId: oldEpisode.podcastId, | ||||
|         audioFile: oldEpisode.audioFile?.toJSON() || null, | ||||
|         chapters: oldEpisode.chapters | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   PodcastEpisode.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     index: DataTypes.INTEGER, | ||||
|     season: DataTypes.STRING, | ||||
|     episode: DataTypes.STRING, | ||||
|     episodeType: DataTypes.STRING, | ||||
|     title: DataTypes.STRING, | ||||
|     subtitle: DataTypes.STRING(1000), | ||||
|     description: DataTypes.TEXT, | ||||
|     pubDate: DataTypes.STRING, | ||||
|     enclosureURL: DataTypes.STRING, | ||||
|     enclosureSize: DataTypes.BIGINT, | ||||
|     enclosureType: DataTypes.STRING, | ||||
|     publishedAt: DataTypes.DATE, | ||||
| 
 | ||||
|     audioFile: DataTypes.JSON, | ||||
|     chapters: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'podcastEpisode' | ||||
|   }) | ||||
| 
 | ||||
|   const { podcast } = sequelize.models | ||||
|   podcast.hasMany(PodcastEpisode) | ||||
|   PodcastEpisode.belongsTo(podcast) | ||||
| 
 | ||||
|   return PodcastEpisode | ||||
| } | ||||
							
								
								
									
										72
									
								
								server/models/Series.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/models/Series.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldSeries = require('../objects/entities/Series') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Series extends Model { | ||||
|     static async getAllOldSeries() { | ||||
|       const series = await this.findAll() | ||||
|       return series.map(se => se.getOldSeries()) | ||||
|     } | ||||
| 
 | ||||
|     getOldSeries() { | ||||
|       return new oldSeries({ | ||||
|         id: this.id, | ||||
|         name: this.name, | ||||
|         description: this.description, | ||||
|         addedAt: this.createdAt.valueOf(), | ||||
|         updatedAt: this.updatedAt.valueOf() | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldSeries) { | ||||
|       const series = this.getFromOld(oldSeries) | ||||
|       return this.update(series, { | ||||
|         where: { | ||||
|           id: series.id | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldSeries) { | ||||
|       const series = this.getFromOld(oldSeries) | ||||
|       return this.create(series) | ||||
|     } | ||||
| 
 | ||||
|     static createBulkFromOld(oldSeriesObjs) { | ||||
|       const series = oldSeriesObjs.map(this.getFromOld) | ||||
|       return this.bulkCreate(series) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldSeries) { | ||||
|       return { | ||||
|         id: oldSeries.id, | ||||
|         name: oldSeries.name, | ||||
|         description: oldSeries.description | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(seriesId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: seriesId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Series.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     name: DataTypes.STRING, | ||||
|     description: DataTypes.TEXT | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'series' | ||||
|   }) | ||||
| 
 | ||||
|   return Series | ||||
| } | ||||
							
								
								
									
										45
									
								
								server/models/Setting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								server/models/Setting.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| 
 | ||||
| const oldEmailSettings = require('../objects/settings/EmailSettings') | ||||
| const oldServerSettings = require('../objects/settings/ServerSettings') | ||||
| const oldNotificationSettings = require('../objects/settings/NotificationSettings') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class Setting extends Model { | ||||
|     static async getOldSettings() { | ||||
|       const settings = (await this.findAll()).map(se => se.value) | ||||
| 
 | ||||
| 
 | ||||
|       const emailSettingsJson = settings.find(se => se.id === 'email-settings') | ||||
|       const serverSettingsJson = settings.find(se => se.id === 'server-settings') | ||||
|       const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') | ||||
| 
 | ||||
|       return { | ||||
|         settings, | ||||
|         emailSettings: new oldEmailSettings(emailSettingsJson), | ||||
|         serverSettings: new oldServerSettings(serverSettingsJson), | ||||
|         notificationSettings: new oldNotificationSettings(notificationSettingsJson) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static updateSettingObj(setting) { | ||||
|       return this.upsert({ | ||||
|         key: setting.id, | ||||
|         value: setting | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Setting.init({ | ||||
|     key: { | ||||
|       type: DataTypes.STRING, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     value: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'setting' | ||||
|   }) | ||||
| 
 | ||||
|   return Setting | ||||
| } | ||||
							
								
								
									
										136
									
								
								server/models/User.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								server/models/User.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const { DataTypes, Model } = require('sequelize') | ||||
| const Logger = require('../Logger') | ||||
| const oldUser = require('../objects/user/User') | ||||
| 
 | ||||
| module.exports = (sequelize) => { | ||||
|   class User extends Model { | ||||
|     static async getOldUsers() { | ||||
|       const users = await this.findAll({ | ||||
|         include: sequelize.models.mediaProgress | ||||
|       }) | ||||
|       return users.map(u => this.getOldUser(u)) | ||||
|     } | ||||
| 
 | ||||
|     static getOldUser(userExpanded) { | ||||
|       const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) | ||||
| 
 | ||||
|       const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] | ||||
|       const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] | ||||
|       const permissions = userExpanded.permissions || {} | ||||
|       delete permissions.librariesAccessible | ||||
|       delete permissions.itemTagsSelected | ||||
| 
 | ||||
|       return new oldUser({ | ||||
|         id: userExpanded.id, | ||||
|         oldUserId: userExpanded.extraData?.oldUserId || null, | ||||
|         username: userExpanded.username, | ||||
|         pash: userExpanded.pash, | ||||
|         type: userExpanded.type, | ||||
|         token: userExpanded.token, | ||||
|         mediaProgress, | ||||
|         seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], | ||||
|         bookmarks: userExpanded.bookmarks, | ||||
|         isActive: userExpanded.isActive, | ||||
|         isLocked: userExpanded.isLocked, | ||||
|         lastSeen: userExpanded.lastSeen?.valueOf() || null, | ||||
|         createdAt: userExpanded.createdAt.valueOf(), | ||||
|         permissions, | ||||
|         librariesAccessible, | ||||
|         itemTagsSelected | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static createFromOld(oldUser) { | ||||
|       const user = this.getFromOld(oldUser) | ||||
|       return this.create(user) | ||||
|     } | ||||
| 
 | ||||
|     static updateFromOld(oldUser) { | ||||
|       const user = this.getFromOld(oldUser) | ||||
|       return this.update(user, { | ||||
|         where: { | ||||
|           id: user.id | ||||
|         } | ||||
|       }).then((result) => result[0] > 0).catch((error) => { | ||||
|         Logger.error(`[User] Failed to save user ${oldUser.id}`, error) | ||||
|         return false | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static getFromOld(oldUser) { | ||||
|       return { | ||||
|         id: oldUser.id, | ||||
|         username: oldUser.username, | ||||
|         pash: oldUser.pash || null, | ||||
|         type: oldUser.type || null, | ||||
|         token: oldUser.token || null, | ||||
|         isActive: !!oldUser.isActive, | ||||
|         lastSeen: oldUser.lastSeen || null, | ||||
|         extraData: { | ||||
|           seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], | ||||
|           oldUserId: oldUser.oldUserId | ||||
|         }, | ||||
|         createdAt: oldUser.createdAt || Date.now(), | ||||
|         permissions: { | ||||
|           ...oldUser.permissions, | ||||
|           librariesAccessible: oldUser.librariesAccessible || [], | ||||
|           itemTagsSelected: oldUser.itemTagsSelected || [] | ||||
|         }, | ||||
|         bookmarks: oldUser.bookmarks | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     static removeById(userId) { | ||||
|       return this.destroy({ | ||||
|         where: { | ||||
|           id: userId | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     static async createRootUser(username, pash, token) { | ||||
|       const newRoot = new oldUser({ | ||||
|         id: uuidv4(), | ||||
|         type: 'root', | ||||
|         username, | ||||
|         pash, | ||||
|         token, | ||||
|         isActive: true, | ||||
|         createdAt: Date.now() | ||||
|       }) | ||||
|       await this.createFromOld(newRoot) | ||||
|       return newRoot | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   User.init({ | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: DataTypes.UUIDV4, | ||||
|       primaryKey: true | ||||
|     }, | ||||
|     username: DataTypes.STRING, | ||||
|     email: DataTypes.STRING, | ||||
|     pash: DataTypes.STRING, | ||||
|     type: DataTypes.STRING, | ||||
|     token: DataTypes.STRING, | ||||
|     isActive: { | ||||
|       type: DataTypes.BOOLEAN, | ||||
|       defaultValue: false | ||||
|     }, | ||||
|     isLocked: { | ||||
|       type: DataTypes.BOOLEAN, | ||||
|       defaultValue: false | ||||
|     }, | ||||
|     lastSeen: DataTypes.DATE, | ||||
|     permissions: DataTypes.JSON, | ||||
|     bookmarks: DataTypes.JSON, | ||||
|     extraData: DataTypes.JSON | ||||
|   }, { | ||||
|     sequelize, | ||||
|     modelName: 'user' | ||||
|   }) | ||||
| 
 | ||||
|   return User | ||||
| } | ||||
| @ -1,10 +1,9 @@ | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Collection { | ||||
|   constructor(collection) { | ||||
|     this.id = null | ||||
|     this.libraryId = null | ||||
|     this.userId = null | ||||
| 
 | ||||
|     this.name = null | ||||
|     this.description = null | ||||
| @ -25,7 +24,6 @@ class Collection { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       libraryId: this.libraryId, | ||||
|       userId: this.userId, | ||||
|       name: this.name, | ||||
|       description: this.description, | ||||
|       cover: this.cover, | ||||
| @ -60,7 +58,6 @@ class Collection { | ||||
|   construct(collection) { | ||||
|     this.id = collection.id | ||||
|     this.libraryId = collection.libraryId | ||||
|     this.userId = collection.userId | ||||
|     this.name = collection.name | ||||
|     this.description = collection.description || null | ||||
|     this.cover = collection.cover || null | ||||
| @ -71,11 +68,10 @@ class Collection { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     if (!data.userId || !data.libraryId || !data.name) { | ||||
|     if (!data.libraryId || !data.name) { | ||||
|       return false | ||||
|     } | ||||
|     this.id = getId('col') | ||||
|     this.userId = data.userId | ||||
|     this.id = uuidv4() | ||||
|     this.libraryId = data.libraryId | ||||
|     this.name = data.name | ||||
|     this.description = data.description || null | ||||
|  | ||||
| @ -1,5 +1,9 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class DeviceInfo { | ||||
|   constructor(deviceInfo = null) { | ||||
|     this.id = null | ||||
|     this.userId = null | ||||
|     this.deviceId = null | ||||
|     this.ipAddress = null | ||||
| 
 | ||||
| @ -16,7 +20,8 @@ class DeviceInfo { | ||||
|     this.model = null | ||||
|     this.sdkVersion = null // Android Only
 | ||||
| 
 | ||||
|     this.serverVersion = null | ||||
|     this.clientName = null | ||||
|     this.deviceName = null | ||||
| 
 | ||||
|     if (deviceInfo) { | ||||
|       this.construct(deviceInfo) | ||||
| @ -33,6 +38,8 @@ class DeviceInfo { | ||||
| 
 | ||||
|   toJSON() { | ||||
|     const obj = { | ||||
|       id: this.id, | ||||
|       userId: this.userId, | ||||
|       deviceId: this.deviceId, | ||||
|       ipAddress: this.ipAddress, | ||||
|       browserName: this.browserName, | ||||
| @ -44,7 +51,8 @@ class DeviceInfo { | ||||
|       manufacturer: this.manufacturer, | ||||
|       model: this.model, | ||||
|       sdkVersion: this.sdkVersion, | ||||
|       serverVersion: this.serverVersion | ||||
|       clientName: this.clientName, | ||||
|       deviceName: this.deviceName | ||||
|     } | ||||
|     for (const key in obj) { | ||||
|       if (obj[key] === null || obj[key] === undefined) { | ||||
| @ -65,6 +73,7 @@ class DeviceInfo { | ||||
|   // When client doesn't send a device id
 | ||||
|   getTempDeviceId() { | ||||
|     const keys = [ | ||||
|       this.userId, | ||||
|       this.browserName, | ||||
|       this.browserVersion, | ||||
|       this.osName, | ||||
| @ -78,7 +87,9 @@ class DeviceInfo { | ||||
|     return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') | ||||
|   } | ||||
| 
 | ||||
|   setData(ip, ua, clientDeviceInfo, serverVersion) { | ||||
|   setData(ip, ua, clientDeviceInfo, serverVersion, userId) { | ||||
|     this.id = uuidv4() | ||||
|     this.userId = userId | ||||
|     this.deviceId = clientDeviceInfo?.deviceId || null | ||||
|     this.ipAddress = ip || null | ||||
| 
 | ||||
| @ -88,16 +99,54 @@ class DeviceInfo { | ||||
|     this.osVersion = ua?.os.version || null | ||||
|     this.deviceType = ua?.device.type || null | ||||
| 
 | ||||
|     this.clientVersion = clientDeviceInfo?.clientVersion || null | ||||
|     this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion | ||||
|     this.manufacturer = clientDeviceInfo?.manufacturer || null | ||||
|     this.model = clientDeviceInfo?.model || null | ||||
|     this.sdkVersion = clientDeviceInfo?.sdkVersion || null | ||||
| 
 | ||||
|     this.serverVersion = serverVersion || null | ||||
|     this.clientName = clientDeviceInfo?.clientName || null | ||||
|     if (this.sdkVersion) { | ||||
|       if (!this.clientName) this.clientName = 'Abs Android' | ||||
|       this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}` | ||||
|     } else if (this.model) { | ||||
|       if (!this.clientName) this.clientName = 'Abs iOS' | ||||
|       this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}` | ||||
|     } else if (this.osName && this.browserName) { | ||||
|       if (!this.clientName) this.clientName = 'Abs Web' | ||||
|       this.deviceName = `${this.osName} ${this.osVersion || 'N/A'} ${this.browserName}` | ||||
|     } else if (!this.clientName) { | ||||
|       this.clientName = 'Unknown' | ||||
|     } | ||||
| 
 | ||||
|     if (!this.deviceId) { | ||||
|       this.deviceId = this.getTempDeviceId() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   update(deviceInfo) { | ||||
|     const deviceInfoJson = deviceInfo.toJSON ? deviceInfo.toJSON() : deviceInfo | ||||
|     const existingDeviceInfoJson = this.toJSON() | ||||
| 
 | ||||
|     let hasUpdates = false | ||||
|     for (const key in deviceInfoJson) { | ||||
|       if (['id', 'deviceId'].includes(key)) continue | ||||
| 
 | ||||
|       if (deviceInfoJson[key] !== existingDeviceInfoJson[key]) { | ||||
|         this[key] = deviceInfoJson[key] | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     for (const key in existingDeviceInfoJson) { | ||||
|       if (['id', 'deviceId'].includes(key)) continue | ||||
| 
 | ||||
|       if (existingDeviceInfoJson[key] && !deviceInfoJson[key]) { | ||||
|         this[key] = null | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return hasUpdates | ||||
|   } | ||||
| } | ||||
| module.exports = DeviceInfo | ||||
| @ -1,3 +1,4 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const FeedMeta = require('./FeedMeta') | ||||
| const FeedEpisode = require('./FeedEpisode') | ||||
| const RSS = require('../libs/rss') | ||||
| @ -90,7 +91,7 @@ class Feed { | ||||
|     const feedUrl = `${serverAddress}/feed/${slug}` | ||||
|     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName | ||||
| 
 | ||||
|     this.id = slug | ||||
|     this.id = uuidv4() | ||||
|     this.slug = slug | ||||
|     this.userId = userId | ||||
|     this.entityType = 'libraryItem' | ||||
| @ -179,7 +180,7 @@ class Feed { | ||||
|     const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||
|     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) | ||||
| 
 | ||||
|     this.id = slug | ||||
|     this.id = uuidv4() | ||||
|     this.slug = slug | ||||
|     this.userId = userId | ||||
|     this.entityType = 'collection' | ||||
| @ -253,7 +254,7 @@ class Feed { | ||||
|     const libraryId = itemsWithTracks[0].libraryId | ||||
|     const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath) | ||||
| 
 | ||||
|     this.id = slug | ||||
|     this.id = uuidv4() | ||||
|     this.slug = slug | ||||
|     this.userId = userId | ||||
|     this.entityType = 'series' | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| const Path = require('path') | ||||
| const date = require('../libs/dateAndTime') | ||||
| const { secondsToTimestamp } = require('../utils/index') | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const { getId } = require("../utils") | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Folder { | ||||
|   constructor(folder = null) { | ||||
| @ -29,7 +29,7 @@ class Folder { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     this.id = data.id ? data.id : getId('fol') | ||||
|     this.id = data.id || uuidv4() | ||||
|     this.fullPath = data.fullPath | ||||
|     this.libraryId = data.libraryId | ||||
|     this.addedAt = Date.now() | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const Folder = require('./Folder') | ||||
| const LibrarySettings = require('./settings/LibrarySettings') | ||||
| const { getId } = require('../utils/index') | ||||
| const { filePathToPOSIX } = require('../utils/fileUtils') | ||||
| 
 | ||||
| class Library { | ||||
| @ -87,7 +87,7 @@ class Library { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     this.id = data.id ? data.id : getId('lib') | ||||
|     this.id = data.id || uuidv4() | ||||
|     this.name = data.name | ||||
|     if (data.folder) { | ||||
|       this.folders = [ | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const fs = require('../libs/fsExtra') | ||||
| const Path = require('path') | ||||
| const { version } = require('../../package.json') | ||||
| @ -8,7 +9,7 @@ const Book = require('./mediaTypes/Book') | ||||
| const Podcast = require('./mediaTypes/Podcast') | ||||
| const Video = require('./mediaTypes/Video') | ||||
| const Music = require('./mediaTypes/Music') | ||||
| const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index') | ||||
| const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index') | ||||
| const { filePathToPOSIX } = require('../utils/fileUtils') | ||||
| 
 | ||||
| class LibraryItem { | ||||
| @ -191,7 +192,7 @@ class LibraryItem { | ||||
| 
 | ||||
|   // Data comes from scandir library item data
 | ||||
|   setData(libraryMediaType, payload) { | ||||
|     this.id = getId('li') | ||||
|     this.id = uuidv4() | ||||
|     this.mediaType = libraryMediaType | ||||
|     if (libraryMediaType === 'video') { | ||||
|       this.media = new Video() | ||||
| @ -202,6 +203,7 @@ class LibraryItem { | ||||
|     } else if (libraryMediaType === 'music') { | ||||
|       this.media = new Music() | ||||
|     } | ||||
|     this.media.id = uuidv4() | ||||
|     this.media.libraryItemId = this.id | ||||
| 
 | ||||
|     for (const key in payload) { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Notification { | ||||
|   constructor(notification = null) { | ||||
| @ -57,7 +57,7 @@ class Notification { | ||||
|   } | ||||
| 
 | ||||
|   setData(payload) { | ||||
|     this.id = getId('noti') | ||||
|     this.id = uuidv4() | ||||
|     this.libraryId = payload.libraryId || null | ||||
|     this.eventName = payload.eventName | ||||
|     this.urls = payload.urls | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const date = require('../libs/dateAndTime') | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const serverVersion = require('../../package.json').version | ||||
| const BookMetadata = require('./metadata/BookMetadata') | ||||
| const PodcastMetadata = require('./metadata/PodcastMetadata') | ||||
| const DeviceInfo = require('./DeviceInfo') | ||||
| @ -11,6 +12,7 @@ class PlaybackSession { | ||||
|     this.userId = null | ||||
|     this.libraryId = null | ||||
|     this.libraryItemId = null | ||||
|     this.bookId = null | ||||
|     this.episodeId = null | ||||
| 
 | ||||
|     this.mediaType = null | ||||
| @ -24,6 +26,7 @@ class PlaybackSession { | ||||
|     this.playMethod = null | ||||
|     this.mediaPlayer = null | ||||
|     this.deviceInfo = null | ||||
|     this.serverVersion = null | ||||
| 
 | ||||
|     this.date = null | ||||
|     this.dayOfWeek = null | ||||
| @ -52,6 +55,7 @@ class PlaybackSession { | ||||
|       userId: this.userId, | ||||
|       libraryId: this.libraryId, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       bookId: this.bookId, | ||||
|       episodeId: this.episodeId, | ||||
|       mediaType: this.mediaType, | ||||
|       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||
| @ -63,6 +67,7 @@ class PlaybackSession { | ||||
|       playMethod: this.playMethod, | ||||
|       mediaPlayer: this.mediaPlayer, | ||||
|       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||
|       serverVersion: this.serverVersion, | ||||
|       date: this.date, | ||||
|       dayOfWeek: this.dayOfWeek, | ||||
|       timeListening: this.timeListening, | ||||
| @ -79,6 +84,7 @@ class PlaybackSession { | ||||
|       userId: this.userId, | ||||
|       libraryId: this.libraryId, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       bookId: this.bookId, | ||||
|       episodeId: this.episodeId, | ||||
|       mediaType: this.mediaType, | ||||
|       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||
| @ -90,6 +96,7 @@ class PlaybackSession { | ||||
|       playMethod: this.playMethod, | ||||
|       mediaPlayer: this.mediaPlayer, | ||||
|       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||
|       serverVersion: this.serverVersion, | ||||
|       date: this.date, | ||||
|       dayOfWeek: this.dayOfWeek, | ||||
|       timeListening: this.timeListening, | ||||
| @ -108,12 +115,20 @@ class PlaybackSession { | ||||
|     this.userId = session.userId | ||||
|     this.libraryId = session.libraryId || null | ||||
|     this.libraryItemId = session.libraryItemId | ||||
|     this.bookId = session.bookId | ||||
|     this.episodeId = session.episodeId | ||||
|     this.mediaType = session.mediaType | ||||
|     this.duration = session.duration | ||||
|     this.playMethod = session.playMethod | ||||
|     this.mediaPlayer = session.mediaPlayer || null | ||||
| 
 | ||||
|     if (session.deviceInfo instanceof DeviceInfo) { | ||||
|       this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON()) | ||||
|     } else { | ||||
|       this.deviceInfo = new DeviceInfo(session.deviceInfo) | ||||
|     } | ||||
| 
 | ||||
|     this.serverVersion = session.serverVersion | ||||
|     this.chapters = session.chapters || [] | ||||
| 
 | ||||
|     this.mediaMetadata = null | ||||
| @ -151,7 +166,7 @@ class PlaybackSession { | ||||
|   } | ||||
| 
 | ||||
|   get deviceId() { | ||||
|     return this.deviceInfo?.deviceId | ||||
|     return this.deviceInfo?.id | ||||
|   } | ||||
| 
 | ||||
|   get deviceDescription() { | ||||
| @ -169,10 +184,11 @@ class PlaybackSession { | ||||
|   } | ||||
| 
 | ||||
|   setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { | ||||
|     this.id = getId('play') | ||||
|     this.id = uuidv4() | ||||
|     this.userId = user.id | ||||
|     this.libraryId = libraryItem.libraryId | ||||
|     this.libraryItemId = libraryItem.id | ||||
|     this.bookId = episodeId ? null : libraryItem.media.id | ||||
|     this.episodeId = episodeId | ||||
|     this.mediaType = libraryItem.mediaType | ||||
|     this.mediaMetadata = libraryItem.media.metadata.clone() | ||||
| @ -189,6 +205,7 @@ class PlaybackSession { | ||||
| 
 | ||||
|     this.mediaPlayer = mediaPlayer | ||||
|     this.deviceInfo = deviceInfo || new DeviceInfo() | ||||
|     this.serverVersion = serverVersion | ||||
| 
 | ||||
|     this.timeListening = 0 | ||||
|     this.startTime = startTime | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| const Logger = require('../Logger') | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Playlist { | ||||
|   constructor(playlist) { | ||||
| @ -88,7 +87,7 @@ class Playlist { | ||||
|     if (!data.userId || !data.libraryId || !data.name) { | ||||
|       return false | ||||
|     } | ||||
|     this.id = getId('pl') | ||||
|     this.id = uuidv4() | ||||
|     this.userId = data.userId | ||||
|     this.libraryId = data.libraryId | ||||
|     this.name = data.name | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| const Path = require('path') | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const { sanitizeFilename } = require('../utils/fileUtils') | ||||
| const globals = require('../utils/globals') | ||||
| 
 | ||||
| @ -70,7 +70,7 @@ class PodcastEpisodeDownload { | ||||
|   } | ||||
| 
 | ||||
|   setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) { | ||||
|     this.id = getId('epdl') | ||||
|     this.id = uuidv4() | ||||
|     this.podcastEpisode = podcastEpisode | ||||
| 
 | ||||
|     const url = podcastEpisode.enclosure.url | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const { getId } = require('../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Task { | ||||
|   constructor() { | ||||
| @ -35,7 +35,7 @@ class Task { | ||||
|   } | ||||
| 
 | ||||
|   setData(action, title, description, showSuccess, data = {}) { | ||||
|     this.id = getId(action) | ||||
|     this.id = uuidv4() | ||||
|     this.action = action | ||||
|     this.data = { ...data } | ||||
|     this.title = title | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| const Logger = require('../../Logger') | ||||
| const { getId } = require('../../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString') | ||||
| 
 | ||||
| class Author { | ||||
| @ -53,7 +53,7 @@ class Author { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     this.id = getId('aut') | ||||
|     this.id = uuidv4() | ||||
|     this.name = data.name | ||||
|     this.description = data.description || null | ||||
|     this.asin = data.asin || null | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const Path = require('path') | ||||
| const Logger = require('../../Logger') | ||||
| const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index') | ||||
| const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index') | ||||
| const AudioFile = require('../files/AudioFile') | ||||
| const AudioTrack = require('../files/AudioTrack') | ||||
| 
 | ||||
| class PodcastEpisode { | ||||
|   constructor(episode) { | ||||
|     this.libraryItemId = null | ||||
|     this.podcastId = null | ||||
|     this.id = null | ||||
|     this.index = null | ||||
| 
 | ||||
| @ -32,6 +34,7 @@ class PodcastEpisode { | ||||
| 
 | ||||
|   construct(episode) { | ||||
|     this.libraryItemId = episode.libraryItemId | ||||
|     this.podcastId = episode.podcastId | ||||
|     this.id = episode.id | ||||
|     this.index = episode.index | ||||
|     this.season = episode.season | ||||
| @ -54,6 +57,7 @@ class PodcastEpisode { | ||||
|   toJSON() { | ||||
|     return { | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       podcastId: this.podcastId, | ||||
|       id: this.id, | ||||
|       index: this.index, | ||||
|       season: this.season, | ||||
| @ -75,6 +79,7 @@ class PodcastEpisode { | ||||
|   toJSONExpanded() { | ||||
|     return { | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       podcastId: this.podcastId, | ||||
|       id: this.id, | ||||
|       index: this.index, | ||||
|       season: this.season, | ||||
| @ -117,7 +122,7 @@ class PodcastEpisode { | ||||
|   } | ||||
| 
 | ||||
|   setData(data, index = 1) { | ||||
|     this.id = getId('ep') | ||||
|     this.id = uuidv4() | ||||
|     this.index = index | ||||
|     this.title = data.title | ||||
|     this.subtitle = data.subtitle || '' | ||||
| @ -133,7 +138,7 @@ class PodcastEpisode { | ||||
|   } | ||||
| 
 | ||||
|   setDataFromAudioFile(audioFile, index) { | ||||
|     this.id = getId('ep') | ||||
|     this.id = uuidv4() | ||||
|     this.audioFile = audioFile | ||||
|     this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename)) | ||||
|     this.index = index | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const { getId } = require('../../utils/index') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class Series { | ||||
|   constructor(series) { | ||||
| @ -40,7 +40,7 @@ class Series { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     this.id = getId('ser') | ||||
|     this.id = uuidv4() | ||||
|     this.name = data.name | ||||
|     this.description = data.description || null | ||||
|     this.addedAt = Date.now() | ||||
|  | ||||
| @ -12,6 +12,7 @@ const EBookFile = require('../files/EBookFile') | ||||
| 
 | ||||
| class Book { | ||||
|   constructor(book) { | ||||
|     this.id = null | ||||
|     this.libraryItemId = null | ||||
|     this.metadata = null | ||||
| 
 | ||||
| @ -32,6 +33,7 @@ class Book { | ||||
|   } | ||||
| 
 | ||||
|   construct(book) { | ||||
|     this.id = book.id | ||||
|     this.libraryItemId = book.libraryItemId | ||||
|     this.metadata = new BookMetadata(book.metadata) | ||||
|     this.coverPath = book.coverPath | ||||
| @ -46,6 +48,7 @@ class Book { | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       metadata: this.metadata.toJSON(), | ||||
|       coverPath: this.coverPath, | ||||
| @ -59,6 +62,7 @@ class Book { | ||||
| 
 | ||||
|   toJSONMinified() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       metadata: this.metadata.toJSONMinified(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
| @ -75,6 +79,7 @@ class Book { | ||||
| 
 | ||||
|   toJSONExpanded() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       metadata: this.metadata.toJSONExpanded(), | ||||
|       coverPath: this.coverPath, | ||||
|  | ||||
| @ -11,6 +11,7 @@ const naturalSort = createNewSortInstance({ | ||||
| 
 | ||||
| class Podcast { | ||||
|   constructor(podcast) { | ||||
|     this.id = null | ||||
|     this.libraryItemId = null | ||||
|     this.metadata = null | ||||
|     this.coverPath = null | ||||
| @ -32,6 +33,7 @@ class Podcast { | ||||
|   } | ||||
| 
 | ||||
|   construct(podcast) { | ||||
|     this.id = podcast.id | ||||
|     this.libraryItemId = podcast.libraryItemId | ||||
|     this.metadata = new PodcastMetadata(podcast.metadata) | ||||
|     this.coverPath = podcast.coverPath | ||||
| @ -50,6 +52,7 @@ class Podcast { | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       metadata: this.metadata.toJSON(), | ||||
|       coverPath: this.coverPath, | ||||
| @ -65,6 +68,7 @@ class Podcast { | ||||
| 
 | ||||
|   toJSONMinified() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       metadata: this.metadata.toJSONMinified(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
| @ -80,6 +84,7 @@ class Podcast { | ||||
| 
 | ||||
|   toJSONExpanded() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       metadata: this.metadata.toJSONExpanded(), | ||||
|       coverPath: this.coverPath, | ||||
| @ -284,8 +289,9 @@ class Podcast { | ||||
|   } | ||||
| 
 | ||||
|   addNewEpisodeFromAudioFile(audioFile, index) { | ||||
|     var pe = new PodcastEpisode() | ||||
|     const pe = new PodcastEpisode() | ||||
|     pe.libraryItemId = this.libraryItemId | ||||
|     pe.podcastId = this.id | ||||
|     audioFile.index = 1 // Only 1 audio file per episode
 | ||||
|     pe.setDataFromAudioFile(audioFile, index) | ||||
|     this.episodes.push(pe) | ||||
|  | ||||
| @ -218,7 +218,7 @@ class BookMetadata { | ||||
| 
 | ||||
|   // Updates author name
 | ||||
|   updateAuthor(updatedAuthor) { | ||||
|     var author = this.authors.find(au => au.id === updatedAuthor.id) | ||||
|     const author = this.authors.find(au => au.id === updatedAuthor.id) | ||||
|     if (!author || author.name == updatedAuthor.name) return false | ||||
|     author.name = updatedAuthor.name | ||||
|     return true | ||||
|  | ||||
| @ -1,9 +1,15 @@ | ||||
| const uuidv4 = require("uuid").v4 | ||||
| 
 | ||||
| class MediaProgress { | ||||
|   constructor(progress) { | ||||
|     this.id = null | ||||
|     this.userId = null | ||||
|     this.libraryItemId = null | ||||
|     this.episodeId = null // For podcasts
 | ||||
| 
 | ||||
|     this.mediaItemId = null // For use in new data model
 | ||||
|     this.mediaItemType = null // For use in new data model
 | ||||
| 
 | ||||
|     this.duration = null | ||||
|     this.progress = null // 0 to 1
 | ||||
|     this.currentTime = null // seconds
 | ||||
| @ -25,8 +31,11 @@ class MediaProgress { | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       userId: this.userId, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       episodeId: this.episodeId, | ||||
|       mediaItemId: this.mediaItemId, | ||||
|       mediaItemType: this.mediaItemType, | ||||
|       duration: this.duration, | ||||
|       progress: this.progress, | ||||
|       currentTime: this.currentTime, | ||||
| @ -42,8 +51,11 @@ class MediaProgress { | ||||
| 
 | ||||
|   construct(progress) { | ||||
|     this.id = progress.id | ||||
|     this.userId = progress.userId | ||||
|     this.libraryItemId = progress.libraryItemId | ||||
|     this.episodeId = progress.episodeId | ||||
|     this.mediaItemId = progress.mediaItemId | ||||
|     this.mediaItemType = progress.mediaItemType | ||||
|     this.duration = progress.duration || 0 | ||||
|     this.progress = progress.progress | ||||
|     this.currentTime = progress.currentTime || 0 | ||||
| @ -60,10 +72,16 @@ class MediaProgress { | ||||
|     return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0)) | ||||
|   } | ||||
| 
 | ||||
|   setData(libraryItemId, progress, episodeId = null) { | ||||
|     this.id = episodeId ? `${libraryItemId}-${episodeId}` : libraryItemId | ||||
|     this.libraryItemId = libraryItemId | ||||
|   setData(libraryItem, progress, episodeId, userId) { | ||||
|     this.id = uuidv4() | ||||
|     this.userId = userId | ||||
|     this.libraryItemId = libraryItem.id | ||||
|     this.episodeId = episodeId | ||||
| 
 | ||||
|     // PodcastEpisodeId or BookId
 | ||||
|     this.mediaItemId = episodeId || libraryItem.media.id | ||||
|     this.mediaItemType = episodeId ? 'podcastEpisode' : 'book' | ||||
| 
 | ||||
|     this.duration = progress.duration || 0 | ||||
|     this.progress = Math.min(1, (progress.progress || 0)) | ||||
|     this.currentTime = progress.currentTime || 0 | ||||
|  | ||||
| @ -5,6 +5,7 @@ const MediaProgress = require('./MediaProgress') | ||||
| class User { | ||||
|   constructor(user) { | ||||
|     this.id = null | ||||
|     this.oldUserId = null // TODO: Temp for keeping old access tokens
 | ||||
|     this.username = null | ||||
|     this.pash = null | ||||
|     this.type = null | ||||
| @ -73,6 +74,7 @@ class User { | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       oldUserId: this.oldUserId, | ||||
|       username: this.username, | ||||
|       pash: this.pash, | ||||
|       type: this.type, | ||||
| @ -93,6 +95,7 @@ class User { | ||||
|   toJSONForBrowser(hideRootToken = false, minimal = false) { | ||||
|     const json = { | ||||
|       id: this.id, | ||||
|       oldUserId: this.oldUserId, | ||||
|       username: this.username, | ||||
|       type: this.type, | ||||
|       token: (this.type === 'root' && hideRootToken) ? '' : this.token, | ||||
| @ -126,6 +129,7 @@ class User { | ||||
|     } | ||||
|     return { | ||||
|       id: this.id, | ||||
|       oldUserId: this.oldUserId, | ||||
|       username: this.username, | ||||
|       type: this.type, | ||||
|       session, | ||||
| @ -137,6 +141,7 @@ class User { | ||||
| 
 | ||||
|   construct(user) { | ||||
|     this.id = user.id | ||||
|     this.oldUserId = user.oldUserId | ||||
|     this.username = user.username | ||||
|     this.pash = user.pash | ||||
|     this.type = user.type | ||||
| @ -320,7 +325,7 @@ class User { | ||||
|     if (!itemProgress) { | ||||
|       const newItemProgress = new MediaProgress() | ||||
| 
 | ||||
|       newItemProgress.setData(libraryItem.id, updatePayload, episodeId) | ||||
|       newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id) | ||||
|       this.mediaProgress.push(newItemProgress) | ||||
|       return true | ||||
|     } | ||||
| @ -336,12 +341,6 @@ class User { | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   removeMediaProgressForLibraryItem(libraryItemId) { | ||||
|     if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false | ||||
|     this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   checkCanAccessLibrary(libraryId) { | ||||
|     if (this.permissions.accessAllLibraries) return true | ||||
|     if (!this.librariesAccessible) return false | ||||
|  | ||||
| @ -2,6 +2,7 @@ const express = require('express') | ||||
| const Path = require('path') | ||||
| 
 | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| 
 | ||||
| const fs = require('../libs/fsExtra') | ||||
| @ -37,7 +38,6 @@ const Series = require('../objects/entities/Series') | ||||
| 
 | ||||
| class ApiRouter { | ||||
|   constructor(Server) { | ||||
|     this.db = Server.db | ||||
|     this.auth = Server.auth | ||||
|     this.scanner = Server.scanner | ||||
|     this.playbackSessionManager = Server.playbackSessionManager | ||||
| @ -356,7 +356,7 @@ class ApiRouter { | ||||
|     const json = user.toJSONForBrowser(hideRootToken) | ||||
| 
 | ||||
|     json.mediaProgress = json.mediaProgress.map(lip => { | ||||
|       const libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId) | ||||
|       const libraryItem = Database.libraryItems.find(li => li.id === lip.libraryItemId) | ||||
|       if (!libraryItem) { | ||||
|         Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId) | ||||
|         lip.media = null | ||||
| @ -381,11 +381,10 @@ class ApiRouter { | ||||
|   } | ||||
| 
 | ||||
|   async handleDeleteLibraryItem(libraryItem) { | ||||
|     // Remove libraryItem from users
 | ||||
|     for (let i = 0; i < this.db.users.length; i++) { | ||||
|       const user = this.db.users[i] | ||||
|       if (user.removeMediaProgressForLibraryItem(libraryItem.id)) { | ||||
|         await this.db.updateEntity('user', user) | ||||
|     // Remove media progress for this library item from all users
 | ||||
|     for (const user of Database.users) { | ||||
|       for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) { | ||||
|         await Database.removeMediaProgress(mediaProgress.id) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| @ -393,12 +392,12 @@ class ApiRouter { | ||||
| 
 | ||||
|     if (libraryItem.isBook) { | ||||
|       // remove book from collections
 | ||||
|       const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id)) | ||||
|       const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id)) | ||||
|       for (let i = 0; i < collectionsWithBook.length; i++) { | ||||
|         const collection = collectionsWithBook[i] | ||||
|         collection.removeBook(libraryItem.id) | ||||
|         await this.db.updateEntity('collection', collection) | ||||
|         SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) | ||||
|         await Database.removeCollectionBook(collection.id, libraryItem.media.id) | ||||
|         SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems)) | ||||
|       } | ||||
| 
 | ||||
|       // Check remove empty series
 | ||||
| @ -406,7 +405,7 @@ class ApiRouter { | ||||
|     } | ||||
| 
 | ||||
|     // remove item from playlists
 | ||||
|     const playlistsWithItem = this.db.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id)) | ||||
|     const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id)) | ||||
|     for (let i = 0; i < playlistsWithItem.length; i++) { | ||||
|       const playlist = playlistsWithItem[i] | ||||
|       playlist.removeItemsForLibraryItem(libraryItem.id) | ||||
| @ -414,11 +413,12 @@ class ApiRouter { | ||||
|       // If playlist is now empty then remove it
 | ||||
|       if (!playlist.items.length) { | ||||
|         Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`) | ||||
|         await this.db.removeEntity('playlist', playlist.id) | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(this.db.libraryItems)) | ||||
|         await Database.removePlaylist(playlist.id) | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems)) | ||||
|       } else { | ||||
|         await this.db.updateEntity('playlist', playlist) | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(this.db.libraryItems)) | ||||
|         await Database.updatePlaylist(playlist) | ||||
| 
 | ||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems)) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| @ -436,7 +436,7 @@ class ApiRouter { | ||||
|       await fs.remove(itemMetadataPath) | ||||
|     } | ||||
| 
 | ||||
|     await this.db.removeLibraryItem(libraryItem.id) | ||||
|     await Database.removeLibraryItem(libraryItem.id) | ||||
|     SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded()) | ||||
|   } | ||||
| 
 | ||||
| @ -444,27 +444,27 @@ class ApiRouter { | ||||
|     if (!seriesToCheck || !seriesToCheck.length) return | ||||
| 
 | ||||
|     for (const series of seriesToCheck) { | ||||
|       const otherLibraryItemsInSeries = this.db.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id)) | ||||
|       const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id)) | ||||
|       if (!otherLibraryItemsInSeries.length) { | ||||
|         // Close open RSS feed for series
 | ||||
|         await this.rssFeedManager.closeFeedForEntityId(series.id) | ||||
|         Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) | ||||
|         await this.db.removeEntity('series', series.id) | ||||
|         await Database.removeSeries(series.id) | ||||
|         // TODO: Socket events for series?
 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async getUserListeningSessionsHelper(userId) { | ||||
|     const userSessions = await this.db.selectUserSessions(userId) | ||||
|     const userSessions = await Database.getPlaybackSessions({ userId }) | ||||
|     return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||
|   } | ||||
| 
 | ||||
|   async getAllSessionsWithUserData() { | ||||
|     const sessions = await this.db.getAllSessions() | ||||
|     const sessions = await Database.getPlaybackSessions() | ||||
|     sessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||
|     return sessions.map(se => { | ||||
|       const user = this.db.users.find(u => u.id === se.userId) | ||||
|       const user = Database.users.find(u => u.id === se.userId) | ||||
|       return { | ||||
|         ...se, | ||||
|         user: user ? { id: user.id, username: user.username } : null | ||||
| @ -533,7 +533,7 @@ class ApiRouter { | ||||
|           } | ||||
| 
 | ||||
|           if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) { | ||||
|             let author = this.db.authors.find(au => au.checkNameEquals(authorName)) | ||||
|             let author = Database.authors.find(au => au.checkNameEquals(authorName)) | ||||
|             if (!author) { | ||||
|               author = new Author() | ||||
|               author.setData(mediaMetadata.authors[i]) | ||||
| @ -546,7 +546,7 @@ class ApiRouter { | ||||
|           } | ||||
|         } | ||||
|         if (newAuthors.length) { | ||||
|           await this.db.insertEntities('author', newAuthors) | ||||
|           await Database.createBulkAuthors(newAuthors) | ||||
|           SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) | ||||
|         } | ||||
|       } | ||||
| @ -562,7 +562,7 @@ class ApiRouter { | ||||
|           } | ||||
| 
 | ||||
|           if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) { | ||||
|             let seriesItem = this.db.series.find(se => se.checkNameEquals(seriesName)) | ||||
|             let seriesItem = Database.series.find(se => se.checkNameEquals(seriesName)) | ||||
|             if (!seriesItem) { | ||||
|               seriesItem = new Series() | ||||
|               seriesItem.setData(mediaMetadata.series[i]) | ||||
| @ -575,7 +575,7 @@ class ApiRouter { | ||||
|           } | ||||
|         } | ||||
|         if (newSeries.length) { | ||||
|           await this.db.insertEntities('series', newSeries) | ||||
|           await Database.createBulkSeries(newSeries) | ||||
|           SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @ -8,8 +8,7 @@ const fs = require('../libs/fsExtra') | ||||
| 
 | ||||
| 
 | ||||
| class HlsRouter { | ||||
|   constructor(db, auth, playbackSessionManager) { | ||||
|     this.db = db | ||||
|   constructor(auth, playbackSessionManager) { | ||||
|     this.auth = auth | ||||
|     this.playbackSessionManager = playbackSessionManager | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										8
									
								
								server/routes/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								server/routes/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| const express = require('express') | ||||
| const libraries = require('./libraries') | ||||
| 
 | ||||
| const router = express.Router() | ||||
| 
 | ||||
| router.use('/libraries', libraries) | ||||
| 
 | ||||
| module.exports = router | ||||
							
								
								
									
										7
									
								
								server/routes/libraries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/routes/libraries.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| const express = require('express') | ||||
| 
 | ||||
| const router = express.Router() | ||||
| 
 | ||||
| // TODO: Add library routes
 | ||||
| 
 | ||||
| module.exports = router | ||||
| @ -1,4 +1,5 @@ | ||||
| const Path = require('path') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const fs = require('../libs/fsExtra') | ||||
| const date = require('../libs/dateAndTime') | ||||
| 
 | ||||
| @ -6,7 +7,7 @@ const Logger = require('../Logger') | ||||
| const Library = require('../objects/Library') | ||||
| const { LogLevel } = require('../utils/constants') | ||||
| const filePerms = require('../utils/filePerms') | ||||
| const { getId, secondsToTimestamp } = require('../utils/index') | ||||
| const { secondsToTimestamp } = require('../utils/index') | ||||
| 
 | ||||
| class LibraryScan { | ||||
|   constructor() { | ||||
| @ -84,7 +85,7 @@ class LibraryScan { | ||||
|   } | ||||
| 
 | ||||
|   setData(library, scanOptions, type = 'scan') { | ||||
|     this.id = getId('lscan') | ||||
|     this.id = uuidv4() | ||||
|     this.type = type | ||||
|     this.library = new Library(library.toJSON()) // clone library
 | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ const fs = require('../libs/fsExtra') | ||||
| const Path = require('path') | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| 
 | ||||
| // Utils
 | ||||
| const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir') | ||||
| @ -22,8 +23,7 @@ const Series = require('../objects/entities/Series') | ||||
| const Task = require('../objects/Task') | ||||
| 
 | ||||
| class Scanner { | ||||
|   constructor(db, coverManager, taskManager) { | ||||
|     this.db = db | ||||
|   constructor(coverManager, taskManager) { | ||||
|     this.coverManager = coverManager | ||||
|     this.taskManager = taskManager | ||||
| 
 | ||||
| @ -66,7 +66,7 @@ class Scanner { | ||||
|   } | ||||
| 
 | ||||
|   async scanLibraryItemByRequest(libraryItem) { | ||||
|     const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId) | ||||
|     const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId) | ||||
|     if (!library) { | ||||
|       Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) | ||||
|       return ScanResult.NOTHING | ||||
| @ -108,7 +108,7 @@ class Scanner { | ||||
|     if (checkRes.updated) hasUpdated = true | ||||
| 
 | ||||
|     // Sync other files first so that local images are used as cover art
 | ||||
|     if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata, library.settings)) { | ||||
|     if (await libraryItem.syncFiles(Database.serverSettings.scannerPreferOpfMetadata, library.settings)) { | ||||
|       hasUpdated = true | ||||
|     } | ||||
| 
 | ||||
| @ -141,7 +141,7 @@ class Scanner { | ||||
|     } | ||||
| 
 | ||||
|     if (hasUpdated) { | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|       return ScanResult.UPDATED | ||||
|     } | ||||
| @ -160,7 +160,7 @@ class Scanner { | ||||
|     } | ||||
| 
 | ||||
|     const scanOptions = new ScanOptions() | ||||
|     scanOptions.setData(options, this.db.serverSettings) | ||||
|     scanOptions.setData(options, Database.serverSettings) | ||||
| 
 | ||||
|     const libraryScan = new LibraryScan() | ||||
|     libraryScan.setData(library, scanOptions) | ||||
| @ -212,7 +212,7 @@ class Scanner { | ||||
| 
 | ||||
|     // Remove items with no inode
 | ||||
|     libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino) | ||||
|     const libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId) | ||||
|     const libraryItemsInLibrary = Database.libraryItems.filter(li => li.libraryId === libraryScan.libraryId) | ||||
| 
 | ||||
|     const MaxSizePerChunk = 2.5e9 | ||||
|     const itemDataToRescanChunks = [] | ||||
| @ -333,7 +333,7 @@ class Scanner { | ||||
|   } | ||||
| 
 | ||||
|   async updateLibraryItemChunk(itemsToUpdate) { | ||||
|     await this.db.updateLibraryItems(itemsToUpdate) | ||||
|     await Database.updateBulkLibraryItems(itemsToUpdate) | ||||
|     SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded())) | ||||
|   } | ||||
| 
 | ||||
| @ -351,7 +351,7 @@ class Scanner { | ||||
| 
 | ||||
|     if (itemsUpdated.length) { | ||||
|       libraryScan.resultsUpdated += itemsUpdated.length | ||||
|       await this.db.updateLibraryItems(itemsUpdated) | ||||
|       await Database.updateBulkLibraryItems(itemsUpdated) | ||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||
|     } | ||||
|   } | ||||
| @ -368,7 +368,7 @@ class Scanner { | ||||
|     } | ||||
| 
 | ||||
|     libraryScan.resultsAdded += newLibraryItems.length | ||||
|     await this.db.insertLibraryItems(newLibraryItems) | ||||
|     await Database.createBulkLibraryItems(newLibraryItems) | ||||
|     SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded())) | ||||
|   } | ||||
| 
 | ||||
| @ -436,6 +436,7 @@ class Scanner { | ||||
| 
 | ||||
|     const libraryItem = new LibraryItem() | ||||
|     libraryItem.setData(library.mediaType, libraryItemData) | ||||
|     libraryItem.setLastScan() | ||||
| 
 | ||||
|     const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video') | ||||
|     if (mediaFiles.length) { | ||||
| @ -478,7 +479,7 @@ class Scanner { | ||||
|     if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) { | ||||
|       var newAuthors = [] | ||||
|       libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => { | ||||
|         var _author = this.db.authors.find(au => au.checkNameEquals(tempMinAuthor.name)) | ||||
|         var _author = Database.authors.find(au => au.checkNameEquals(tempMinAuthor.name)) | ||||
|         if (!_author) _author = newAuthors.find(au => au.checkNameEquals(tempMinAuthor.name)) // Check new unsaved authors
 | ||||
|         if (!_author) { // Must create new author
 | ||||
|           _author = new Author() | ||||
| @ -492,14 +493,14 @@ class Scanner { | ||||
|         } | ||||
|       }) | ||||
|       if (newAuthors.length) { | ||||
|         await this.db.insertEntities('author', newAuthors) | ||||
|         await Database.createBulkAuthors(newAuthors) | ||||
|         SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) | ||||
|       } | ||||
|     } | ||||
|     if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) { | ||||
|       var newSeries = [] | ||||
|       libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => { | ||||
|         var _series = this.db.series.find(se => se.checkNameEquals(tempMinSeries.name)) | ||||
|         var _series = Database.series.find(se => se.checkNameEquals(tempMinSeries.name)) | ||||
|         if (!_series) _series = newSeries.find(se => se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series
 | ||||
|         if (!_series) { // Must create new series
 | ||||
|           _series = new Series() | ||||
| @ -513,7 +514,7 @@ class Scanner { | ||||
|         } | ||||
|       }) | ||||
|       if (newSeries.length) { | ||||
|         await this.db.insertEntities('series', newSeries) | ||||
|         await Database.createBulkSeries(newSeries) | ||||
|         SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) | ||||
|       } | ||||
|     } | ||||
| @ -551,7 +552,7 @@ class Scanner { | ||||
| 
 | ||||
|     for (const folderId in folderGroups) { | ||||
|       const libraryId = folderGroups[folderId].libraryId | ||||
|       const library = this.db.libraries.find(lib => lib.id === libraryId) | ||||
|       const library = Database.libraries.find(lib => lib.id === libraryId) | ||||
|       if (!library) { | ||||
|         Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) | ||||
|         continue; | ||||
| @ -597,12 +598,12 @@ class Scanner { | ||||
|       const altDir = `${itemDir}/${firstNest}` | ||||
| 
 | ||||
|       const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) | ||||
|       const childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath)) | ||||
|       const childLibraryItem = Database.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath)) | ||||
|       if (!childLibraryItem) { | ||||
|         continue | ||||
|       } | ||||
|       const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir) | ||||
|       const altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath)) | ||||
|       const altChildLibraryItem = Database.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath)) | ||||
|       if (altChildLibraryItem) { | ||||
|         continue | ||||
|       } | ||||
| @ -619,9 +620,9 @@ class Scanner { | ||||
|       const dirIno = await getIno(fullPath) | ||||
| 
 | ||||
|       // Check if book dir group is already an item
 | ||||
|       let existingLibraryItem = this.db.libraryItems.find(li => fullPath.startsWith(li.path)) | ||||
|       let existingLibraryItem = Database.libraryItems.find(li => fullPath.startsWith(li.path)) | ||||
|       if (!existingLibraryItem) { | ||||
|         existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno) | ||||
|         existingLibraryItem = Database.libraryItems.find(li => li.ino === dirIno) | ||||
|         if (existingLibraryItem) { | ||||
|           Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`) | ||||
|           // Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
 | ||||
| @ -636,7 +637,7 @@ class Scanner { | ||||
|           if (!exists) { | ||||
|             Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`) | ||||
|             existingLibraryItem.setMissing() | ||||
|             await this.db.updateLibraryItem(existingLibraryItem) | ||||
|             await Database.updateLibraryItem(existingLibraryItem) | ||||
|             SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|             itemGroupingResults[itemDir] = ScanResult.REMOVED | ||||
| @ -654,7 +655,7 @@ class Scanner { | ||||
|       } | ||||
| 
 | ||||
|       // Check if a library item is a subdirectory of this dir
 | ||||
|       var childItem = this.db.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/')) | ||||
|       var childItem = Database.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/')) | ||||
|       if (childItem) { | ||||
|         Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`) | ||||
|         itemGroupingResults[itemDir] = ScanResult.NOTHING | ||||
| @ -666,7 +667,7 @@ class Scanner { | ||||
|       var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem) | ||||
|       if (newLibraryItem) { | ||||
|         await this.createNewAuthorsAndSeries(newLibraryItem) | ||||
|         await this.db.insertLibraryItem(newLibraryItem) | ||||
|         await Database.createLibraryItem(newLibraryItem) | ||||
|         SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded()) | ||||
|       } | ||||
|       itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING | ||||
| @ -686,7 +687,7 @@ class Scanner { | ||||
|       titleDistance: 2, | ||||
|       authorDistance: 2 | ||||
|     } | ||||
|     const scannerCoverProvider = this.db.serverSettings.scannerCoverProvider | ||||
|     const scannerCoverProvider = Database.serverSettings.scannerCoverProvider | ||||
|     const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options) | ||||
|     if (results.length) { | ||||
|       if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`) | ||||
| @ -716,7 +717,7 @@ class Scanner { | ||||
| 
 | ||||
|     // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and 
 | ||||
|     // the overrideDefaults option is not set or set to false.
 | ||||
|     if ((overrideDefaults == false) && (this.db.serverSettings.scannerPreferMatchedMetadata)) { | ||||
|     if ((overrideDefaults == false) && (Database.serverSettings.scannerPreferMatchedMetadata)) { | ||||
|       options.overrideCover = true | ||||
|       options.overrideDetails = true | ||||
|     } | ||||
| @ -783,7 +784,7 @@ class Scanner { | ||||
|         await this.quickMatchPodcastEpisodes(libraryItem, options) | ||||
|       } | ||||
| 
 | ||||
|       await this.db.updateLibraryItem(libraryItem) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     } | ||||
| 
 | ||||
| @ -878,11 +879,11 @@ class Scanner { | ||||
|       const authorPayload = [] | ||||
|       for (let index = 0; index < matchData.author.length; index++) { | ||||
|         const authorName = matchData.author[index] | ||||
|         var author = this.db.authors.find(au => au.checkNameEquals(authorName)) | ||||
|         var author = Database.authors.find(au => au.checkNameEquals(authorName)) | ||||
|         if (!author) { | ||||
|           author = new Author() | ||||
|           author.setData({ name: authorName }) | ||||
|           await this.db.insertEntity('author', author) | ||||
|           await Database.createAuthor(author) | ||||
|           SocketAuthority.emitter('author_added', author.toJSON()) | ||||
|         } | ||||
|         authorPayload.push(author.toJSONMinimal()) | ||||
| @ -896,11 +897,11 @@ class Scanner { | ||||
|       const seriesPayload = [] | ||||
|       for (let index = 0; index < matchData.series.length; index++) { | ||||
|         const seriesMatchItem = matchData.series[index] | ||||
|         var seriesItem = this.db.series.find(au => au.checkNameEquals(seriesMatchItem.series)) | ||||
|         var seriesItem = Database.series.find(au => au.checkNameEquals(seriesMatchItem.series)) | ||||
|         if (!seriesItem) { | ||||
|           seriesItem = new Series() | ||||
|           seriesItem.setData({ name: seriesMatchItem.series }) | ||||
|           await this.db.insertEntity('series', seriesItem) | ||||
|           await Database.createSeries(seriesItem) | ||||
|           SocketAuthority.emitter('series_added', seriesItem.toJSON()) | ||||
|         } | ||||
|         seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) | ||||
| @ -981,7 +982,7 @@ class Scanner { | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     var itemsInLibrary = this.db.getLibraryItemsInLibrary(library.id) | ||||
|     const itemsInLibrary = Database.libraryItems.filter(li => li.libraryId === library.id) | ||||
|     if (!itemsInLibrary.length) { | ||||
|       Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) | ||||
|       return | ||||
|  | ||||
| @ -17,24 +17,31 @@ | ||||
|  @param value2 Other item to compare | ||||
|  @param stack Used internally to track circular refs - don't set it | ||||
|  */ | ||||
| module.exports = function areEquivalent(value1, value2, stack = []) { | ||||
| module.exports = function areEquivalent(value1, value2, numToString = false, stack = []) { | ||||
|   if (numToString) { | ||||
|     if (value1 !== null && !isNaN(value1)) value1 = String(value1) | ||||
|     if (value2 !== null && !isNaN(value2)) value2 = String(value2) | ||||
|   } | ||||
| 
 | ||||
|   // Numbers, strings, null, undefined, symbols, functions, booleans.
 | ||||
|   // Also: objects (incl. arrays) that are actually the same instance
 | ||||
|   if (value1 === value2) { | ||||
|     // Fast and done
 | ||||
|     return true; | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   // Truthy check to handle value1=null, value2=Object
 | ||||
|   if ((value1 && !value2) || (!value1 && value2)) { | ||||
|     console.log('value1/value2 falsy mismatch', value1, value2) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   const type1 = typeof value1; | ||||
|   const type1 = typeof value1 | ||||
| 
 | ||||
|   // Ensure types match
 | ||||
|   if (type1 !== typeof value2) { | ||||
|     return false; | ||||
|     console.log('type diff', type1, typeof value2) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // Special case for number: check for NaN on both sides
 | ||||
| @ -49,26 +56,27 @@ module.exports = function areEquivalent(value1, value2, stack = []) { | ||||
|     // Failed initial equals test, but could still have equivalent
 | ||||
|     // implementations - note, will match on functions that have same name
 | ||||
|     // and are native code: `function abc() { [native code] }`
 | ||||
|     return value1.toString() === value2.toString(); | ||||
|     return value1.toString() === value2.toString() | ||||
|   } | ||||
| 
 | ||||
|   // For these types, cannot still be equal at this point, so fast-fail
 | ||||
|   if (type1 === 'bigint' || type1 === 'boolean' || | ||||
|     type1 === 'function' || type1 === 'string' || | ||||
|     type1 === 'symbol') { | ||||
|     return false; | ||||
|     console.log('no match for values', value1, value2) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // For dates, cast to number and ensure equal or both NaN (note, if same
 | ||||
|   // exact instance then we're not here - that was checked above)
 | ||||
|   if (value1 instanceof Date) { | ||||
|     if (!(value2 instanceof Date)) { | ||||
|       return false; | ||||
|       return false | ||||
|     } | ||||
|     // Convert to number to compare
 | ||||
|     const asNum1 = +value1, asNum2 = +value2; | ||||
|     const asNum1 = +value1, asNum2 = +value2 | ||||
|     // Check if both invalid (NaN) or are same value
 | ||||
|     return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2)); | ||||
|     return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2)) | ||||
|   } | ||||
| 
 | ||||
|   // At this point, it's a reference type and could be circular, so
 | ||||
| @ -80,61 +88,67 @@ module.exports = function areEquivalent(value1, value2, stack = []) { | ||||
|   } | ||||
| 
 | ||||
|   // breadcrumb
 | ||||
|   stack.push(value1); | ||||
|   stack.push(value1) | ||||
| 
 | ||||
|   // Handle arrays
 | ||||
|   if (Array.isArray(value1)) { | ||||
|     if (!Array.isArray(value2)) { | ||||
|       return false; | ||||
|       console.log('value2 is not array but value1 is', value1, value2) | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     const length = value1.length; | ||||
|     const length = value1.length | ||||
| 
 | ||||
|     if (length !== value2.length) { | ||||
|       return false; | ||||
|       console.log('array length diff', length) | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     for (let i = 0; i < length; i++) { | ||||
|       if (!areEquivalent(value1[i], value2[i], stack)) { | ||||
|         return false; | ||||
|       if (!areEquivalent(value1[i], value2[i], numToString, stack)) { | ||||
|         console.log('2 array items are not equiv', value1[i], value2[i]) | ||||
|         return false | ||||
|       } | ||||
|     } | ||||
|     return true; | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   // Final case: object
 | ||||
| 
 | ||||
|   // get both key lists and check length
 | ||||
|   const keys1 = Object.keys(value1); | ||||
|   const keys2 = Object.keys(value2); | ||||
|   const numKeys = keys1.length; | ||||
|   const keys1 = Object.keys(value1) | ||||
|   const keys2 = Object.keys(value2) | ||||
|   const numKeys = keys1.length | ||||
| 
 | ||||
|   if (keys2.length !== numKeys) { | ||||
|     return false; | ||||
|     console.log('Key length is diff', keys2.length, numKeys) | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // Empty object on both sides?
 | ||||
|   if (numKeys === 0) { | ||||
|     return true; | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   // sort is a native call so it's very fast - much faster than comparing the
 | ||||
|   // values at each key if it can be avoided, so do the sort and then
 | ||||
|   // ensure every key matches at every index
 | ||||
|   keys1.sort(); | ||||
|   keys2.sort(); | ||||
|   keys1.sort() | ||||
|   keys2.sort() | ||||
| 
 | ||||
|   // Ensure perfect match across all keys
 | ||||
|   for (let i = 0; i < numKeys; i++) { | ||||
|     if (keys1[i] !== keys2[i]) { | ||||
|       return false; | ||||
|       console.log('object key is not equiv', keys1[i], keys2[i]) | ||||
|       return false | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Ensure perfect match across all values
 | ||||
|   for (let i = 0; i < numKeys; i++) { | ||||
|     if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) { | ||||
|       return false; | ||||
|     if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) { | ||||
|       console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]]) | ||||
|       return false | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| const { sort, createNewSortInstance } = require('../libs/fastSort') | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index') | ||||
| const naturalSort = createNewSortInstance({ | ||||
|   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare | ||||
| @ -574,7 +575,7 @@ module.exports = { | ||||
|             const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id) | ||||
| 
 | ||||
|             if (!seriesMap[librarySeries.id]) { | ||||
|               const seriesObj = ctx.db.series.find(se => se.id === librarySeries.id) | ||||
|               const seriesObj = Database.series.find(se => se.id === librarySeries.id) | ||||
|               if (seriesObj) { | ||||
|                 const series = { | ||||
|                   ...seriesObj.toJSON(), | ||||
| @ -626,7 +627,7 @@ module.exports = { | ||||
|         if (libraryItem.media.metadata.authors.length) { | ||||
|           for (const libraryAuthor of libraryItem.media.metadata.authors) { | ||||
|             if (!authorMap[libraryAuthor.id]) { | ||||
|               const authorObj = ctx.db.authors.find(au => au.id === libraryAuthor.id) | ||||
|               const authorObj = Database.authors.find(au => au.id === libraryAuthor.id) | ||||
|               if (authorObj) { | ||||
|                 const author = { | ||||
|                   ...authorObj.toJSON(), | ||||
|  | ||||
							
								
								
									
										763
									
								
								server/utils/migrations/dbMigration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										763
									
								
								server/utils/migrations/dbMigration.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,763 @@ | ||||
| const Path = require('path') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const Logger = require('../../Logger') | ||||
| const fs = require('../../libs/fsExtra') | ||||
| const oldDbFiles = require('./oldDbFiles') | ||||
| 
 | ||||
| const oldDbIdMap = { | ||||
|   users: {}, | ||||
|   libraries: {}, | ||||
|   libraryFolders: {}, | ||||
|   libraryItems: {}, | ||||
|   authors: {}, | ||||
|   series: {}, | ||||
|   collections: {}, | ||||
|   podcastEpisodes: {}, | ||||
|   books: {}, // key is library item id
 | ||||
|   podcasts: {}, // key is library item id
 | ||||
|   devices: {} // key is a json stringify of the old DeviceInfo data OR deviceId if it exists
 | ||||
| } | ||||
| const newRecords = { | ||||
|   user: [], | ||||
|   library: [], | ||||
|   libraryFolder: [], | ||||
|   author: [], | ||||
|   book: [], | ||||
|   podcast: [], | ||||
|   libraryItem: [], | ||||
|   bookAuthor: [], | ||||
|   series: [], | ||||
|   bookSeries: [], | ||||
|   podcastEpisode: [], | ||||
|   mediaProgress: [], | ||||
|   device: [], | ||||
|   playbackSession: [], | ||||
|   collection: [], | ||||
|   collectionBook: [], | ||||
|   playlist: [], | ||||
|   playlistMediaItem: [], | ||||
|   feed: [], | ||||
|   feedEpisode: [], | ||||
|   setting: [] | ||||
| } | ||||
| 
 | ||||
| function getDeviceInfoString(deviceInfo, UserId) { | ||||
|   if (!deviceInfo) return null | ||||
|   if (deviceInfo.deviceId) return deviceInfo.deviceId | ||||
| 
 | ||||
|   const keys = [ | ||||
|     UserId, | ||||
|     deviceInfo.browserName || null, | ||||
|     deviceInfo.browserVersion || null, | ||||
|     deviceInfo.osName || null, | ||||
|     deviceInfo.osVersion || null, | ||||
|     deviceInfo.clientVersion || null, | ||||
|     deviceInfo.manufacturer || null, | ||||
|     deviceInfo.model || null, | ||||
|     deviceInfo.sdkVersion || null, | ||||
|     deviceInfo.ipAddress || null | ||||
|   ].map(k => k || '') | ||||
|   return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') | ||||
| } | ||||
| 
 | ||||
| function migrateBook(oldLibraryItem, LibraryItem) { | ||||
|   const oldBook = oldLibraryItem.media | ||||
| 
 | ||||
|   //
 | ||||
|   // Migrate Book
 | ||||
|   //
 | ||||
|   const Book = { | ||||
|     id: uuidv4(), | ||||
|     title: oldBook.metadata.title, | ||||
|     subtitle: oldBook.metadata.subtitle, | ||||
|     publishedYear: oldBook.metadata.publishedYear, | ||||
|     publishedDate: oldBook.metadata.publishedDate, | ||||
|     publisher: oldBook.metadata.publisher, | ||||
|     description: oldBook.metadata.description, | ||||
|     isbn: oldBook.metadata.isbn, | ||||
|     asin: oldBook.metadata.asin, | ||||
|     language: oldBook.metadata.language, | ||||
|     explicit: !!oldBook.metadata.explicit, | ||||
|     abridged: !!oldBook.metadata.abridged, | ||||
|     lastCoverSearchQuery: oldBook.lastCoverSearchQuery, | ||||
|     lastCoverSearch: oldBook.lastCoverSearch, | ||||
|     createdAt: LibraryItem.createdAt, | ||||
|     updatedAt: LibraryItem.updatedAt, | ||||
|     narrators: oldBook.metadata.narrators, | ||||
|     ebookFile: oldBook.ebookFile, | ||||
|     coverPath: oldBook.coverPath, | ||||
|     audioFiles: oldBook.audioFiles, | ||||
|     chapters: oldBook.chapters, | ||||
|     tags: oldBook.tags, | ||||
|     genres: oldBook.metadata.genres | ||||
|   } | ||||
|   newRecords.book.push(Book) | ||||
|   oldDbIdMap.books[oldLibraryItem.id] = Book.id | ||||
| 
 | ||||
|   //
 | ||||
|   // Migrate BookAuthors
 | ||||
|   //
 | ||||
|   for (const oldBookAuthor of oldBook.metadata.authors) { | ||||
|     if (oldDbIdMap.authors[oldBookAuthor.id]) { | ||||
|       newRecords.bookAuthor.push({ | ||||
|         id: uuidv4(), | ||||
|         authorId: oldDbIdMap.authors[oldBookAuthor.id], | ||||
|         bookId: Book.id | ||||
|       }) | ||||
|     } else { | ||||
|       Logger.warn(`[dbMigration] migrateBook: Book author not found "${oldBookAuthor.name}"`) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   //
 | ||||
|   // Migrate BookSeries
 | ||||
|   //
 | ||||
|   for (const oldBookSeries of oldBook.metadata.series) { | ||||
|     if (oldDbIdMap.series[oldBookSeries.id]) { | ||||
|       const BookSeries = { | ||||
|         id: uuidv4(), | ||||
|         sequence: oldBookSeries.sequence, | ||||
|         seriesId: oldDbIdMap.series[oldBookSeries.id], | ||||
|         bookId: Book.id | ||||
|       } | ||||
|       newRecords.bookSeries.push(BookSeries) | ||||
|     } else { | ||||
|       Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migratePodcast(oldLibraryItem, LibraryItem) { | ||||
|   const oldPodcast = oldLibraryItem.media | ||||
|   const oldPodcastMetadata = oldPodcast.metadata | ||||
| 
 | ||||
|   //
 | ||||
|   // Migrate Podcast
 | ||||
|   //
 | ||||
|   const Podcast = { | ||||
|     id: uuidv4(), | ||||
|     title: oldPodcastMetadata.title, | ||||
|     author: oldPodcastMetadata.author, | ||||
|     releaseDate: oldPodcastMetadata.releaseDate, | ||||
|     feedURL: oldPodcastMetadata.feedUrl, | ||||
|     imageURL: oldPodcastMetadata.imageUrl, | ||||
|     description: oldPodcastMetadata.description, | ||||
|     itunesPageURL: oldPodcastMetadata.itunesPageUrl, | ||||
|     itunesId: oldPodcastMetadata.itunesId, | ||||
|     itunesArtistId: oldPodcastMetadata.itunesArtistId, | ||||
|     language: oldPodcastMetadata.language, | ||||
|     podcastType: oldPodcastMetadata.type, | ||||
|     explicit: !!oldPodcastMetadata.explicit, | ||||
|     autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, | ||||
|     autoDownloadSchedule: oldPodcast.autoDownloadSchedule, | ||||
|     lastEpisodeCheck: oldPodcast.lastEpisodeCheck, | ||||
|     maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, | ||||
|     maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, | ||||
|     lastCoverSearchQuery: oldPodcast.lastCoverSearchQuery, | ||||
|     lastCoverSearch: oldPodcast.lastCoverSearch, | ||||
|     createdAt: LibraryItem.createdAt, | ||||
|     updatedAt: LibraryItem.updatedAt, | ||||
|     coverPath: oldPodcast.coverPath, | ||||
|     tags: oldPodcast.tags, | ||||
|     genres: oldPodcastMetadata.genres | ||||
|   } | ||||
|   newRecords.podcast.push(Podcast) | ||||
|   oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id | ||||
| 
 | ||||
|   //
 | ||||
|   // Migrate PodcastEpisodes
 | ||||
|   //
 | ||||
|   const oldEpisodes = oldPodcast.episodes || [] | ||||
|   for (const oldEpisode of oldEpisodes) { | ||||
|     const PodcastEpisode = { | ||||
|       id: uuidv4(), | ||||
|       index: oldEpisode.index, | ||||
|       season: oldEpisode.season, | ||||
|       episode: oldEpisode.episode, | ||||
|       episodeType: oldEpisode.episodeType, | ||||
|       title: oldEpisode.title, | ||||
|       subtitle: oldEpisode.subtitle, | ||||
|       description: oldEpisode.description, | ||||
|       pubDate: oldEpisode.pubDate, | ||||
|       enclosureURL: oldEpisode.enclosure?.url || null, | ||||
|       enclosureSize: oldEpisode.enclosure?.length || null, | ||||
|       enclosureType: oldEpisode.enclosure?.type || null, | ||||
|       publishedAt: oldEpisode.publishedAt, | ||||
|       createdAt: oldEpisode.addedAt, | ||||
|       updatedAt: oldEpisode.updatedAt, | ||||
|       podcastId: Podcast.id, | ||||
|       audioFile: oldEpisode.audioFile, | ||||
|       chapters: oldEpisode.chapters | ||||
|     } | ||||
|     newRecords.podcastEpisode.push(PodcastEpisode) | ||||
|     oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateLibraryItems(oldLibraryItems) { | ||||
|   for (const oldLibraryItem of oldLibraryItems) { | ||||
|     const libraryFolderId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId] | ||||
|     if (!libraryFolderId) { | ||||
|       Logger.error(`[dbMigration] migrateLibraryItems: Old library folder id not found "${oldLibraryItem.folderId}"`) | ||||
|       continue | ||||
|     } | ||||
|     const libraryId = oldDbIdMap.libraries[oldLibraryItem.libraryId] | ||||
|     if (!libraryId) { | ||||
|       Logger.error(`[dbMigration] migrateLibraryItems: Old library id not found "${oldLibraryItem.libraryId}"`) | ||||
|       continue | ||||
|     } | ||||
|     if (!['book', 'podcast'].includes(oldLibraryItem.mediaType)) { | ||||
|       Logger.error(`[dbMigration] migrateLibraryItems: Not migrating library item with mediaType=${oldLibraryItem.mediaType}`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     //
 | ||||
|     // Migrate LibraryItem
 | ||||
|     //
 | ||||
|     const LibraryItem = { | ||||
|       id: uuidv4(), | ||||
|       ino: oldLibraryItem.ino, | ||||
|       path: oldLibraryItem.path, | ||||
|       relPath: oldLibraryItem.relPath, | ||||
|       mediaId: null, // set below
 | ||||
|       mediaType: oldLibraryItem.mediaType, | ||||
|       isFile: !!oldLibraryItem.isFile, | ||||
|       isMissing: !!oldLibraryItem.isMissing, | ||||
|       isInvalid: !!oldLibraryItem.isInvalid, | ||||
|       mtime: oldLibraryItem.mtimeMs, | ||||
|       ctime: oldLibraryItem.ctimeMs, | ||||
|       birthtime: oldLibraryItem.birthtimeMs, | ||||
|       lastScan: oldLibraryItem.lastScan, | ||||
|       lastScanVersion: oldLibraryItem.scanVersion, | ||||
|       createdAt: oldLibraryItem.addedAt, | ||||
|       updatedAt: oldLibraryItem.updatedAt, | ||||
|       libraryId, | ||||
|       libraryFolderId, | ||||
|       libraryFiles: oldLibraryItem.libraryFiles.map(lf => { | ||||
|         if (lf.isSupplementary === undefined) lf.isSupplementary = null | ||||
|         return lf | ||||
|       }) | ||||
|     } | ||||
|     oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id | ||||
|     newRecords.libraryItem.push(LibraryItem) | ||||
| 
 | ||||
|     // 
 | ||||
|     // Migrate Book/Podcast
 | ||||
|     //
 | ||||
|     if (oldLibraryItem.mediaType === 'book') { | ||||
|       migrateBook(oldLibraryItem, LibraryItem) | ||||
|       LibraryItem.mediaId = oldDbIdMap.books[oldLibraryItem.id] | ||||
|     } else if (oldLibraryItem.mediaType === 'podcast') { | ||||
|       migratePodcast(oldLibraryItem, LibraryItem) | ||||
|       LibraryItem.mediaId = oldDbIdMap.podcasts[oldLibraryItem.id] | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateLibraries(oldLibraries) { | ||||
|   for (const oldLibrary of oldLibraries) { | ||||
|     if (!['book', 'podcast'].includes(oldLibrary.mediaType)) { | ||||
|       Logger.error(`[dbMigration] migrateLibraries: Not migrating library with mediaType=${oldLibrary.mediaType}`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     // 
 | ||||
|     // Migrate Library
 | ||||
|     //
 | ||||
|     const Library = { | ||||
|       id: uuidv4(), | ||||
|       name: oldLibrary.name, | ||||
|       displayOrder: oldLibrary.displayOrder, | ||||
|       icon: oldLibrary.icon || null, | ||||
|       mediaType: oldLibrary.mediaType || null, | ||||
|       provider: oldLibrary.provider, | ||||
|       settings: oldLibrary.settings || {}, | ||||
|       createdAt: oldLibrary.createdAt, | ||||
|       updatedAt: oldLibrary.lastUpdate | ||||
|     } | ||||
|     oldDbIdMap.libraries[oldLibrary.id] = Library.id | ||||
|     newRecords.library.push(Library) | ||||
| 
 | ||||
|     // 
 | ||||
|     // Migrate LibraryFolders
 | ||||
|     //
 | ||||
|     for (const oldFolder of oldLibrary.folders) { | ||||
|       const LibraryFolder = { | ||||
|         id: uuidv4(), | ||||
|         path: oldFolder.fullPath, | ||||
|         createdAt: oldFolder.addedAt, | ||||
|         updatedAt: oldLibrary.lastUpdate, | ||||
|         libraryId: Library.id | ||||
|       } | ||||
|       oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id | ||||
|       newRecords.libraryFolder.push(LibraryFolder) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateAuthors(oldAuthors) { | ||||
|   for (const oldAuthor of oldAuthors) { | ||||
|     const Author = { | ||||
|       id: uuidv4(), | ||||
|       name: oldAuthor.name, | ||||
|       asin: oldAuthor.asin || null, | ||||
|       description: oldAuthor.description, | ||||
|       imagePath: oldAuthor.imagePath, | ||||
|       createdAt: oldAuthor.addedAt || Date.now(), | ||||
|       updatedAt: oldAuthor.updatedAt || Date.now() | ||||
|     } | ||||
|     oldDbIdMap.authors[oldAuthor.id] = Author.id | ||||
|     newRecords.author.push(Author) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateSeries(oldSerieses) { | ||||
|   for (const oldSeries of oldSerieses) { | ||||
|     const Series = { | ||||
|       id: uuidv4(), | ||||
|       name: oldSeries.name, | ||||
|       description: oldSeries.description || null, | ||||
|       createdAt: oldSeries.addedAt || Date.now(), | ||||
|       updatedAt: oldSeries.updatedAt || Date.now() | ||||
|     } | ||||
|     oldDbIdMap.series[oldSeries.id] = Series.id | ||||
|     newRecords.series.push(Series) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateUsers(oldUsers) { | ||||
|   for (const oldUser of oldUsers) { | ||||
|     // 
 | ||||
|     // Migrate User
 | ||||
|     //
 | ||||
|     const User = { | ||||
|       id: uuidv4(), | ||||
|       username: oldUser.username, | ||||
|       pash: oldUser.pash || null, | ||||
|       type: oldUser.type || null, | ||||
|       token: oldUser.token || null, | ||||
|       isActive: !!oldUser.isActive, | ||||
|       lastSeen: oldUser.lastSeen || null, | ||||
|       extraData: { | ||||
|         seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [], | ||||
|         oldUserId: oldUser.id // Used to keep old tokens
 | ||||
|       }, | ||||
|       createdAt: oldUser.createdAt || Date.now(), | ||||
|       permissions: { | ||||
|         ...oldUser.permissions, | ||||
|         librariesAccessible: oldUser.librariesAccessible || [], | ||||
|         itemTagsSelected: oldUser.itemTagsSelected || [] | ||||
|       }, | ||||
|       bookmarks: oldUser.bookmarks | ||||
|     } | ||||
|     oldDbIdMap.users[oldUser.id] = User.id | ||||
|     newRecords.user.push(User) | ||||
| 
 | ||||
|     // 
 | ||||
|     // Migrate MediaProgress
 | ||||
|     //
 | ||||
|     for (const oldMediaProgress of oldUser.mediaProgress) { | ||||
|       let mediaItemType = 'book' | ||||
|       let mediaItemId = null | ||||
|       if (oldMediaProgress.episodeId) { | ||||
|         mediaItemType = 'podcastEpisode' | ||||
|         mediaItemId = oldDbIdMap.podcastEpisodes[oldMediaProgress.episodeId] | ||||
|       } else { | ||||
|         mediaItemId = oldDbIdMap.books[oldMediaProgress.libraryItemId] | ||||
|       } | ||||
| 
 | ||||
|       if (!mediaItemId) { | ||||
|         Logger.warn(`[dbMigration] migrateUsers: Unable to find media item for media progress "${oldMediaProgress.id}"`) | ||||
|         continue | ||||
|       } | ||||
| 
 | ||||
|       const MediaProgress = { | ||||
|         id: uuidv4(), | ||||
|         mediaItemId, | ||||
|         mediaItemType, | ||||
|         duration: oldMediaProgress.duration, | ||||
|         currentTime: oldMediaProgress.currentTime, | ||||
|         ebookLocation: oldMediaProgress.ebookLocation || null, | ||||
|         ebookProgress: oldMediaProgress.ebookProgress || null, | ||||
|         isFinished: !!oldMediaProgress.isFinished, | ||||
|         hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, | ||||
|         finishedAt: oldMediaProgress.finishedAt, | ||||
|         createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, | ||||
|         updatedAt: oldMediaProgress.lastUpdate, | ||||
|         userId: User.id, | ||||
|         extraData: { | ||||
|           libraryItemId: oldDbIdMap.libraryItems[oldMediaProgress.libraryItemId], | ||||
|           progress: oldMediaProgress.progress | ||||
|         } | ||||
|       } | ||||
|       newRecords.mediaProgress.push(MediaProgress) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateSessions(oldSessions) { | ||||
|   for (const oldSession of oldSessions) { | ||||
|     const userId = oldDbIdMap.users[oldSession.userId] || null // Can be null
 | ||||
| 
 | ||||
|     //
 | ||||
|     // Migrate Device
 | ||||
|     //
 | ||||
|     let deviceId = null | ||||
|     if (oldSession.deviceInfo) { | ||||
|       const oldDeviceInfo = oldSession.deviceInfo | ||||
|       const deviceDeviceId = getDeviceInfoString(oldDeviceInfo, userId) | ||||
|       deviceId = oldDbIdMap.devices[deviceDeviceId] | ||||
|       if (!deviceId) { | ||||
|         let clientName = 'Unknown' | ||||
|         let clientVersion = null | ||||
|         let deviceName = null | ||||
|         let deviceVersion = oldDeviceInfo.browserVersion || null | ||||
|         let extraData = {} | ||||
|         if (oldDeviceInfo.sdkVersion) { | ||||
|           clientName = 'Abs Android' | ||||
|           clientVersion = oldDeviceInfo.clientVersion || null | ||||
|           deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}` | ||||
|           deviceVersion = oldDeviceInfo.sdkVersion | ||||
|         } else if (oldDeviceInfo.model) { | ||||
|           clientName = 'Abs iOS' | ||||
|           clientVersion = oldDeviceInfo.clientVersion || null | ||||
|           deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}` | ||||
|         } else if (oldDeviceInfo.osName && oldDeviceInfo.browserName) { | ||||
|           clientName = 'Abs Web' | ||||
|           clientVersion = oldDeviceInfo.serverVersion || null | ||||
|           deviceName = `${oldDeviceInfo.osName} ${oldDeviceInfo.osVersion || 'N/A'} ${oldDeviceInfo.browserName}` | ||||
|         } | ||||
| 
 | ||||
|         if (oldDeviceInfo.manufacturer) { | ||||
|           extraData.manufacturer = oldDeviceInfo.manufacturer | ||||
|         } | ||||
|         if (oldDeviceInfo.model) { | ||||
|           extraData.model = oldDeviceInfo.model | ||||
|         } | ||||
|         if (oldDeviceInfo.osName) { | ||||
|           extraData.osName = oldDeviceInfo.osName | ||||
|         } | ||||
|         if (oldDeviceInfo.osVersion) { | ||||
|           extraData.osVersion = oldDeviceInfo.osVersion | ||||
|         } | ||||
|         if (oldDeviceInfo.browserName) { | ||||
|           extraData.browserName = oldDeviceInfo.browserName | ||||
|         } | ||||
| 
 | ||||
|         const id = uuidv4() | ||||
|         const Device = { | ||||
|           id, | ||||
|           deviceId: deviceDeviceId, | ||||
|           clientName, | ||||
|           clientVersion, | ||||
|           ipAddress: oldDeviceInfo.ipAddress, | ||||
|           deviceName, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
 | ||||
|           deviceVersion, | ||||
|           userId, | ||||
|           extraData | ||||
|         } | ||||
|         newRecords.device.push(Device) | ||||
|         oldDbIdMap.devices[deviceDeviceId] = Device.id | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     //
 | ||||
|     // Migrate PlaybackSession
 | ||||
|     //
 | ||||
|     let mediaItemId = null | ||||
|     let mediaItemType = 'book' | ||||
|     if (oldSession.mediaType === 'podcast') { | ||||
|       mediaItemId = oldDbIdMap.podcastEpisodes[oldSession.episodeId] || null | ||||
|       mediaItemType = 'podcastEpisode' | ||||
|     } else { | ||||
|       mediaItemId = oldDbIdMap.books[oldSession.libraryItemId] || null | ||||
|     } | ||||
| 
 | ||||
|     const PlaybackSession = { | ||||
|       id: uuidv4(), | ||||
|       mediaItemId, // Can be null
 | ||||
|       mediaItemType, | ||||
|       libraryId: oldDbIdMap.libraries[oldSession.libraryId] || null, | ||||
|       displayTitle: oldSession.displayTitle, | ||||
|       displayAuthor: oldSession.displayAuthor, | ||||
|       duration: oldSession.duration, | ||||
|       playMethod: oldSession.playMethod, | ||||
|       mediaPlayer: oldSession.mediaPlayer, | ||||
|       startTime: oldSession.startTime, | ||||
|       currentTime: oldSession.currentTime, | ||||
|       serverVersion: oldSession.deviceInfo?.serverVersion || null, | ||||
|       createdAt: oldSession.startedAt, | ||||
|       updatedAt: oldSession.updatedAt, | ||||
|       userId, // Can be null
 | ||||
|       deviceId, | ||||
|       timeListening: oldSession.timeListening, | ||||
|       coverPath: oldSession.coverPath, | ||||
|       mediaMetadata: oldSession.mediaMetadata, | ||||
|       date: oldSession.date, | ||||
|       dayOfWeek: oldSession.dayOfWeek, | ||||
|       extraData: { | ||||
|         libraryItemId: oldDbIdMap.libraryItems[oldSession.libraryItemId] | ||||
|       } | ||||
|     } | ||||
|     newRecords.playbackSession.push(PlaybackSession) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateCollections(oldCollections) { | ||||
|   for (const oldCollection of oldCollections) { | ||||
|     const libraryId = oldDbIdMap.libraries[oldCollection.libraryId] | ||||
|     if (!libraryId) { | ||||
|       Logger.warn(`[dbMigration] migrateCollections: Library not found for collection "${oldCollection.name}" (id:${oldCollection.libraryId})`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid) | ||||
|     if (!BookIds.length) { | ||||
|       Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const Collection = { | ||||
|       id: uuidv4(), | ||||
|       name: oldCollection.name, | ||||
|       description: oldCollection.description, | ||||
|       createdAt: oldCollection.createdAt, | ||||
|       updatedAt: oldCollection.lastUpdate, | ||||
|       libraryId | ||||
|     } | ||||
|     oldDbIdMap.collections[oldCollection.id] = Collection.id | ||||
|     newRecords.collection.push(Collection) | ||||
| 
 | ||||
|     let order = 1 | ||||
|     BookIds.forEach((bookId) => { | ||||
|       const CollectionBook = { | ||||
|         id: uuidv4(), | ||||
|         createdAt: Collection.createdAt, | ||||
|         bookId, | ||||
|         collectionId: Collection.id, | ||||
|         order: order++ | ||||
|       } | ||||
|       newRecords.collectionBook.push(CollectionBook) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migratePlaylists(oldPlaylists) { | ||||
|   for (const oldPlaylist of oldPlaylists) { | ||||
|     const libraryId = oldDbIdMap.libraries[oldPlaylist.libraryId] | ||||
|     if (!libraryId) { | ||||
|       Logger.warn(`[dbMigration] migratePlaylists: Library not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.libraryId})`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const userId = oldDbIdMap.users[oldPlaylist.userId] | ||||
|     if (!userId) { | ||||
|       Logger.warn(`[dbMigration] migratePlaylists: User not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.userId})`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     let mediaItemType = 'book' | ||||
|     let MediaItemIds = [] | ||||
|     oldPlaylist.items.forEach((itemObj) => { | ||||
|       if (itemObj.episodeId) { | ||||
|         mediaItemType = 'podcastEpisode' | ||||
|         if (oldDbIdMap.podcastEpisodes[itemObj.episodeId]) { | ||||
|           MediaItemIds.push(oldDbIdMap.podcastEpisodes[itemObj.episodeId]) | ||||
|         } | ||||
|       } else if (oldDbIdMap.books[itemObj.libraryItemId]) { | ||||
|         MediaItemIds.push(oldDbIdMap.books[itemObj.libraryItemId]) | ||||
|       } | ||||
|     }) | ||||
|     if (!MediaItemIds.length) { | ||||
|       Logger.warn(`[dbMigration] migratePlaylists: Playlist "${oldPlaylist.name}" has no items`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const Playlist = { | ||||
|       id: uuidv4(), | ||||
|       name: oldPlaylist.name, | ||||
|       description: oldPlaylist.description, | ||||
|       createdAt: oldPlaylist.createdAt, | ||||
|       updatedAt: oldPlaylist.lastUpdate, | ||||
|       userId, | ||||
|       libraryId | ||||
|     } | ||||
|     newRecords.playlist.push(Playlist) | ||||
| 
 | ||||
|     let order = 1 | ||||
|     MediaItemIds.forEach((mediaItemId) => { | ||||
|       const PlaylistMediaItem = { | ||||
|         id: uuidv4(), | ||||
|         mediaItemId, | ||||
|         mediaItemType, | ||||
|         createdAt: Playlist.createdAt, | ||||
|         playlistId: Playlist.id, | ||||
|         order: order++ | ||||
|       } | ||||
|       newRecords.playlistMediaItem.push(PlaylistMediaItem) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateFeeds(oldFeeds) { | ||||
|   for (const oldFeed of oldFeeds) { | ||||
|     if (!oldFeed.episodes?.length) { | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     let entityId = null | ||||
| 
 | ||||
|     if (oldFeed.entityType === 'collection') { | ||||
|       entityId = oldDbIdMap.collections[oldFeed.entityId] | ||||
|     } else if (oldFeed.entityType === 'libraryItem') { | ||||
|       entityId = oldDbIdMap.libraryItems[oldFeed.entityId] | ||||
|     } else if (oldFeed.entityType === 'series') { | ||||
|       entityId = oldDbIdMap.series[oldFeed.entityId] | ||||
|     } | ||||
| 
 | ||||
|     if (!entityId) { | ||||
|       Logger.warn(`[dbMigration] migrateFeeds: Entity not found for feed "${oldFeed.entityType}" (id:${oldFeed.entityId})`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const userId = oldDbIdMap.users[oldFeed.userId] | ||||
|     if (!userId) { | ||||
|       Logger.warn(`[dbMigration] migrateFeeds: User not found for feed (id:${oldFeed.userId})`) | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     const oldFeedMeta = oldFeed.meta | ||||
| 
 | ||||
|     const Feed = { | ||||
|       id: uuidv4(), | ||||
|       slug: oldFeed.slug, | ||||
|       entityType: oldFeed.entityType, | ||||
|       entityId, | ||||
|       entityUpdatedAt: oldFeed.entityUpdatedAt, | ||||
|       serverAddress: oldFeed.serverAddress, | ||||
|       feedURL: oldFeed.feedUrl, | ||||
|       imageURL: oldFeedMeta.imageUrl, | ||||
|       siteURL: oldFeedMeta.link, | ||||
|       title: oldFeedMeta.title, | ||||
|       description: oldFeedMeta.description, | ||||
|       author: oldFeedMeta.author, | ||||
|       podcastType: oldFeedMeta.type || null, | ||||
|       language: oldFeedMeta.language || null, | ||||
|       ownerName: oldFeedMeta.ownerName || null, | ||||
|       ownerEmail: oldFeedMeta.ownerEmail || null, | ||||
|       explicit: !!oldFeedMeta.explicit, | ||||
|       preventIndexing: !!oldFeedMeta.preventIndexing, | ||||
|       createdAt: oldFeed.createdAt, | ||||
|       updatedAt: oldFeed.updatedAt, | ||||
|       userId | ||||
|     } | ||||
|     newRecords.feed.push(Feed) | ||||
| 
 | ||||
|     //
 | ||||
|     // Migrate FeedEpisodes
 | ||||
|     //
 | ||||
|     for (const oldFeedEpisode of oldFeed.episodes) { | ||||
|       const FeedEpisode = { | ||||
|         id: uuidv4(), | ||||
|         title: oldFeedEpisode.title, | ||||
|         author: oldFeedEpisode.author, | ||||
|         description: oldFeedEpisode.description, | ||||
|         siteURL: oldFeedEpisode.link, | ||||
|         enclosureURL: oldFeedEpisode.enclosure?.url || null, | ||||
|         enclosureType: oldFeedEpisode.enclosure?.type || null, | ||||
|         enclosureSize: oldFeedEpisode.enclosure?.size || null, | ||||
|         pubDate: oldFeedEpisode.pubDate, | ||||
|         season: oldFeedEpisode.season || null, | ||||
|         episode: oldFeedEpisode.episode || null, | ||||
|         episodeType: oldFeedEpisode.episodeType || null, | ||||
|         duration: oldFeedEpisode.duration, | ||||
|         filePath: oldFeedEpisode.fullPath, | ||||
|         explicit: !!oldFeedEpisode.explicit, | ||||
|         createdAt: oldFeed.createdAt, | ||||
|         updatedAt: oldFeed.updatedAt, | ||||
|         feedId: Feed.id | ||||
|       } | ||||
|       newRecords.feedEpisode.push(FeedEpisode) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function migrateSettings(oldSettings) { | ||||
|   const serverSettings = oldSettings.find(s => s.id === 'server-settings') | ||||
|   const notificationSettings = oldSettings.find(s => s.id === 'notification-settings') | ||||
|   const emailSettings = oldSettings.find(s => s.id === 'email-settings') | ||||
| 
 | ||||
|   if (serverSettings) { | ||||
|     newRecords.setting.push({ | ||||
|       key: 'server-settings', | ||||
|       value: serverSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   if (notificationSettings) { | ||||
|     newRecords.setting.push({ | ||||
|       key: 'notification-settings', | ||||
|       value: notificationSettings | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   if (emailSettings) { | ||||
|     newRecords.setting.push({ | ||||
|       key: 'email-settings', | ||||
|       value: emailSettings | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports.migrate = async (DatabaseModels) => { | ||||
|   Logger.info(`[dbMigration] Starting migration`) | ||||
| 
 | ||||
|   const data = await oldDbFiles.init() | ||||
| 
 | ||||
|   const start = Date.now() | ||||
|   migrateSettings(data.settings) | ||||
|   migrateAuthors(data.authors) | ||||
|   migrateSeries(data.series) | ||||
|   migrateLibraries(data.libraries) | ||||
|   migrateLibraryItems(data.libraryItems) | ||||
|   migrateUsers(data.users) | ||||
|   migrateSessions(data.sessions) | ||||
|   migrateCollections(data.collections) | ||||
|   migratePlaylists(data.playlists) | ||||
|   migrateFeeds(data.feeds) | ||||
| 
 | ||||
|   let totalRecords = 0 | ||||
|   for (const model in newRecords) { | ||||
|     Logger.info(`[dbMigration] Inserting ${newRecords[model].length} ${model} rows`) | ||||
|     if (newRecords[model].length) { | ||||
|       await DatabaseModels[model].bulkCreate(newRecords[model]) | ||||
|       totalRecords += newRecords[model].length | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const elapsed = Date.now() - start | ||||
| 
 | ||||
|   // Purge author images and cover images from cache
 | ||||
|   try { | ||||
|     const CachePath = Path.join(global.MetadataPath, 'cache') | ||||
|     await fs.emptyDir(Path.join(CachePath, 'covers')) | ||||
|     await fs.emptyDir(Path.join(CachePath, 'images')) | ||||
|   } catch (error) { | ||||
|     Logger.error(`[dbMigration] Failed to purge author/cover image cache`, error) | ||||
|   } | ||||
| 
 | ||||
|   // Put all old db folders into a zipfile oldDb.zip
 | ||||
|   await oldDbFiles.zipWrapOldDb() | ||||
| 
 | ||||
|   Logger.info(`[dbMigration] Migration complete. ${totalRecords} rows. Elapsed ${(elapsed / 1000).toFixed(2)}s`) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @returns {boolean} true if old database exists | ||||
|  */ | ||||
| module.exports.checkShouldMigrate = async (force = false) => { | ||||
|   if (await oldDbFiles.checkHasOldDb()) return true | ||||
|   if (!force) return false | ||||
|   return oldDbFiles.checkHasOldDbZip() | ||||
| } | ||||
							
								
								
									
										189
									
								
								server/utils/migrations/oldDbFiles.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								server/utils/migrations/oldDbFiles.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,189 @@ | ||||
| const { once } = require('events') | ||||
| const { createInterface } = require('readline') | ||||
| const Path = require('path') | ||||
| const Logger = require('../../Logger') | ||||
| const fs = require('../../libs/fsExtra') | ||||
| const archiver = require('../../libs/archiver') | ||||
| const StreamZip = require('../../libs/nodeStreamZip') | ||||
| 
 | ||||
| async function processDbFile(filepath) { | ||||
|   if (!fs.pathExistsSync(filepath)) { | ||||
|     Logger.error(`[oldDbFiles] Db file does not exist at "${filepath}"`) | ||||
|     return [] | ||||
|   } | ||||
| 
 | ||||
|   const entities = [] | ||||
| 
 | ||||
|   try { | ||||
|     const fileStream = fs.createReadStream(filepath) | ||||
| 
 | ||||
|     const rl = createInterface({ | ||||
|       input: fileStream, | ||||
|       crlfDelay: Infinity, | ||||
|     }) | ||||
| 
 | ||||
|     rl.on('line', (line) => { | ||||
|       if (line && line.trim()) { | ||||
|         try { | ||||
|           const entity = JSON.parse(line) | ||||
|           if (entity && Object.keys(entity).length) entities.push(entity) | ||||
|         } catch (jsonParseError) { | ||||
|           Logger.error(`[oldDbFiles] Failed to parse line "${line}" in db file "${filepath}"`, jsonParseError) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
| 
 | ||||
|     await once(rl, 'close') | ||||
| 
 | ||||
|     console.log(`[oldDbFiles] Db file "${filepath}" processed`) | ||||
| 
 | ||||
|     return entities | ||||
|   } catch (error) { | ||||
|     Logger.error(`[oldDbFiles] Failed to read db file "${filepath}"`, error) | ||||
|     return [] | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function loadDbData(dbpath) { | ||||
|   try { | ||||
|     Logger.info(`[oldDbFiles] Loading db data at "${dbpath}"`) | ||||
|     const files = await fs.readdir(dbpath) | ||||
| 
 | ||||
|     const entities = [] | ||||
|     for (const filename of files) { | ||||
|       if (Path.extname(filename).toLowerCase() !== '.json') { | ||||
|         Logger.warn(`[oldDbFiles] Ignoring filename "${filename}" in db folder "${dbpath}"`) | ||||
|         continue | ||||
|       } | ||||
| 
 | ||||
|       const filepath = Path.join(dbpath, filename) | ||||
|       Logger.info(`[oldDbFiles] Loading db data file "${filepath}"`) | ||||
|       const someEntities = await processDbFile(filepath) | ||||
|       Logger.info(`[oldDbFiles] Processed db data file with ${someEntities.length} entities`) | ||||
|       entities.push(...someEntities) | ||||
|     } | ||||
| 
 | ||||
|     Logger.info(`[oldDbFiles] Finished loading db data with ${entities.length} entities`) | ||||
|     return entities | ||||
|   } catch (error) { | ||||
|     Logger.error(`[oldDbFiles] Failed to load db data "${dbpath}"`, error) | ||||
|     return null | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports.init = async () => { | ||||
|   const dbs = { | ||||
|     libraryItems: Path.join(global.ConfigPath, 'libraryItems', 'data'), | ||||
|     users: Path.join(global.ConfigPath, 'users', 'data'), | ||||
|     sessions: Path.join(global.ConfigPath, 'sessions', 'data'), | ||||
|     libraries: Path.join(global.ConfigPath, 'libraries', 'data'), | ||||
|     settings: Path.join(global.ConfigPath, 'settings', 'data'), | ||||
|     collections: Path.join(global.ConfigPath, 'collections', 'data'), | ||||
|     playlists: Path.join(global.ConfigPath, 'playlists', 'data'), | ||||
|     authors: Path.join(global.ConfigPath, 'authors', 'data'), | ||||
|     series: Path.join(global.ConfigPath, 'series', 'data'), | ||||
|     feeds: Path.join(global.ConfigPath, 'feeds', 'data') | ||||
|   } | ||||
| 
 | ||||
|   const data = {} | ||||
|   for (const key in dbs) { | ||||
|     data[key] = await loadDbData(dbs[key]) | ||||
|     Logger.info(`[oldDbFiles] ${data[key].length} ${key} loaded`) | ||||
|   } | ||||
| 
 | ||||
|   return data | ||||
| } | ||||
| 
 | ||||
| module.exports.zipWrapOldDb = async () => { | ||||
|   const dbs = { | ||||
|     libraryItems: Path.join(global.ConfigPath, 'libraryItems'), | ||||
|     users: Path.join(global.ConfigPath, 'users'), | ||||
|     sessions: Path.join(global.ConfigPath, 'sessions'), | ||||
|     libraries: Path.join(global.ConfigPath, 'libraries'), | ||||
|     settings: Path.join(global.ConfigPath, 'settings'), | ||||
|     collections: Path.join(global.ConfigPath, 'collections'), | ||||
|     playlists: Path.join(global.ConfigPath, 'playlists'), | ||||
|     authors: Path.join(global.ConfigPath, 'authors'), | ||||
|     series: Path.join(global.ConfigPath, 'series'), | ||||
|     feeds: Path.join(global.ConfigPath, 'feeds') | ||||
|   } | ||||
| 
 | ||||
|   return new Promise((resolve) => { | ||||
|     const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip') | ||||
|     const output = fs.createWriteStream(oldDbPath) | ||||
|     const archive = archiver('zip', { | ||||
|       zlib: { level: 9 } // Sets the compression level.
 | ||||
|     }) | ||||
| 
 | ||||
|     // listen for all archive data to be written
 | ||||
|     // 'close' event is fired only when a file descriptor is involved
 | ||||
|     output.on('close', async () => { | ||||
|       Logger.info(`[oldDbFiles] Old db files have been zipped in ${oldDbPath}. ${archive.pointer()} total bytes`) | ||||
| 
 | ||||
|       // Remove old db folders have successful zip
 | ||||
|       for (const db in dbs) { | ||||
|         await fs.remove(dbs[db]) | ||||
|       } | ||||
| 
 | ||||
|       resolve(true) | ||||
|     }) | ||||
| 
 | ||||
|     // This event is fired when the data source is drained no matter what was the data source.
 | ||||
|     // It is not part of this library but rather from the NodeJS Stream API.
 | ||||
|     // @see: https://nodejs.org/api/stream.html#stream_event_end
 | ||||
|     output.on('end', () => { | ||||
|       Logger.debug('[oldDbFiles] Data has been drained') | ||||
|     }) | ||||
| 
 | ||||
|     // good practice to catch this error explicitly
 | ||||
|     archive.on('error', (err) => { | ||||
|       Logger.error(`[oldDbFiles] Failed to zip old db folders`, err) | ||||
|       resolve(false) | ||||
|     }) | ||||
| 
 | ||||
|     // pipe archive data to the file
 | ||||
|     archive.pipe(output) | ||||
| 
 | ||||
|     for (const db in dbs) { | ||||
|       archive.directory(dbs[db], db) | ||||
|     } | ||||
| 
 | ||||
|     // finalize the archive (ie we are done appending files but streams have to finish yet)
 | ||||
|     // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
 | ||||
|     archive.finalize() | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| module.exports.checkHasOldDb = async () => { | ||||
|   const dbs = { | ||||
|     libraryItems: Path.join(global.ConfigPath, 'libraryItems'), | ||||
|     users: Path.join(global.ConfigPath, 'users'), | ||||
|     sessions: Path.join(global.ConfigPath, 'sessions'), | ||||
|     libraries: Path.join(global.ConfigPath, 'libraries'), | ||||
|     settings: Path.join(global.ConfigPath, 'settings'), | ||||
|     collections: Path.join(global.ConfigPath, 'collections'), | ||||
|     playlists: Path.join(global.ConfigPath, 'playlists'), | ||||
|     authors: Path.join(global.ConfigPath, 'authors'), | ||||
|     series: Path.join(global.ConfigPath, 'series'), | ||||
|     feeds: Path.join(global.ConfigPath, 'feeds') | ||||
|   } | ||||
|   for (const db in dbs) { | ||||
|     if (await fs.pathExists(dbs[db])) { | ||||
|       return true | ||||
|     } | ||||
|   } | ||||
|   return false | ||||
| } | ||||
| 
 | ||||
| module.exports.checkHasOldDbZip = async () => { | ||||
|   const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip') | ||||
|   if (!await fs.pathExists(oldDbPath)) { | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   // Extract oldDb.zip
 | ||||
|   const zip = new StreamZip.async({ file: oldDbPath }) | ||||
|   await zip.extract(null, global.ConfigPath) | ||||
| 
 | ||||
|   return this.checkHasOldDb() | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user