mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27: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.deletingDeviceName = device.name | ||||||
|       this.$axios |       this.$axios | ||||||
|         .$patch(`/emails/ereader-devices`, payload) |         .$post(`/api/emails/ereader-devices`, payload) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|           this.ereaderDevicesUpdated(data.ereaderDevices) |           this.ereaderDevicesUpdated(data.ereaderDevices) | ||||||
|           this.$toast.success('Device deleted') |           this.$toast.success('Device deleted') | ||||||
|  | |||||||
| @ -191,6 +191,7 @@ export default class PlayerHandler { | |||||||
| 
 | 
 | ||||||
|     const payload = { |     const payload = { | ||||||
|       deviceInfo: { |       deviceInfo: { | ||||||
|  |         clientName: 'Abs Web', | ||||||
|         deviceId: this.getDeviceId() |         deviceId: this.getDeviceId() | ||||||
|       }, |       }, | ||||||
|       supportedMimeTypes: this.player.playableMimeTypes, |       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", |     "htmlparser2": "^8.0.1", | ||||||
|     "node-tone": "^1.0.1", |     "node-tone": "^1.0.1", | ||||||
|     "nodemailer": "^6.9.2", |     "nodemailer": "^6.9.2", | ||||||
|  |     "sequelize": "^6.32.1", | ||||||
|     "socket.io": "^4.5.4", |     "socket.io": "^4.5.4", | ||||||
|  |     "sqlite3": "^5.1.6", | ||||||
|     "xml2js": "^0.5.0" |     "xml2js": "^0.5.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  | |||||||
| @ -2,21 +2,10 @@ const bcrypt = require('./libs/bcryptjs') | |||||||
| const jwt = require('./libs/jsonwebtoken') | const jwt = require('./libs/jsonwebtoken') | ||||||
| const requestIp = require('./libs/requestIp') | const requestIp = require('./libs/requestIp') | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
|  | const Database = require('./Database') | ||||||
| 
 | 
 | ||||||
| class Auth { | class Auth { | ||||||
|   constructor(db) { |   constructor() { } | ||||||
|     this.db = db |  | ||||||
| 
 |  | ||||||
|     this.user = null |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   get username() { |  | ||||||
|     return this.user ? this.user.username : 'nobody' |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   get users() { |  | ||||||
|     return this.db.users |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   cors(req, res, next) { |   cors(req, res, next) { | ||||||
|     res.header('Access-Control-Allow-Origin', '*') |     res.header('Access-Control-Allow-Origin', '*') | ||||||
| @ -35,20 +24,20 @@ class Auth { | |||||||
|   async initTokenSecret() { |   async initTokenSecret() { | ||||||
|     if (process.env.TOKEN_SECRET) { // User can supply their own token secret
 |     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`) |       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 { |     } else { | ||||||
|       Logger.debug(`[Auth] Setting token secret - using random bytes`) |       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
 |     // New token secret creation added in v2.1.0 so generate new API tokens for each user
 | ||||||
|     if (this.db.users.length) { |     if (Database.users.length) { | ||||||
|       for (const user of this.db.users) { |       for (const user of Database.users) { | ||||||
|         user.token = await this.generateAccessToken({ userId: user.id, username: user.username }) |         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`) |         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) |       return res.sendStatus(401) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var user = await this.verifyToken(token) |     const user = await this.verifyToken(token) | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       Logger.error('Verify Token User Not Found', token) |       Logger.error('Verify Token User Not Found', token) | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
| @ -95,7 +84,7 @@ class Auth { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   generateAccessToken(payload) { |   generateAccessToken(payload) { | ||||||
|     return jwt.sign(payload, global.ServerSettings.tokenSecret); |     return jwt.sign(payload, Database.serverSettings.tokenSecret) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   authenticateUser(token) { |   authenticateUser(token) { | ||||||
| @ -104,12 +93,12 @@ class Auth { | |||||||
| 
 | 
 | ||||||
|   verifyToken(token) { |   verifyToken(token) { | ||||||
|     return new Promise((resolve) => { |     return new Promise((resolve) => { | ||||||
|       jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => { |       jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => { | ||||||
|         if (!payload || err) { |         if (!payload || err) { | ||||||
|           Logger.error('JWT Verify Token Failed', err) |           Logger.error('JWT Verify Token Failed', err) | ||||||
|           return resolve(null) |           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) |         resolve(user || null) | ||||||
|       }) |       }) | ||||||
|     }) |     }) | ||||||
| @ -118,9 +107,9 @@ class Auth { | |||||||
|   getUserLoginResponsePayload(user) { |   getUserLoginResponsePayload(user) { | ||||||
|     return { |     return { | ||||||
|       user: user.toJSONForBrowser(), |       user: user.toJSONForBrowser(), | ||||||
|       userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), |       userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries), | ||||||
|       serverSettings: this.db.serverSettings.toJSONForBrowser(), |       serverSettings: Database.serverSettings.toJSONForBrowser(), | ||||||
|       ereaderDevices: this.db.emailSettings.getEReaderDevices(user), |       ereaderDevices: Database.emailSettings.getEReaderDevices(user), | ||||||
|       Source: global.Source |       Source: global.Source | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -130,7 +119,7 @@ class Auth { | |||||||
|     const username = (req.body.username || '').toLowerCase() |     const username = (req.body.username || '').toLowerCase() | ||||||
|     const password = req.body.password || '' |     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) { |     if (!user?.isActive) { | ||||||
|       Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) |       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
 |     // Check passwordless root user
 | ||||||
|     if (user.id === 'root' && (!user.pash || user.pash === '')) { |     if (user.type === 'root' && (!user.pash || user.pash === '')) { | ||||||
|       if (password) { |       if (password) { | ||||||
|         return res.status(401).send('Invalid root password (hint: there is none)') |         return res.status(401).send('Invalid root password (hint: there is none)') | ||||||
|       } else { |       } 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) { |   comparePassword(password, user) { | ||||||
|     if (user.type === 'root' && !password && !user.pash) return true |     if (user.type === 'root' && !password && !user.pash) return true | ||||||
|     if (!password || !user.pash) return false |     if (!password || !user.pash) return false | ||||||
| @ -184,7 +164,7 @@ class Auth { | |||||||
|   async userChangePassword(req, res) { |   async userChangePassword(req, res) { | ||||||
|     var { password, newPassword } = req.body |     var { password, newPassword } = req.body | ||||||
|     newPassword = newPassword || '' |     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
 |     // Only root can have an empty password
 | ||||||
|     if (matchingUser.type !== 'root' && !newPassword) { |     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) { |     if (!compare) { | ||||||
|       return res.json({ |       return res.json({ | ||||||
|         error: 'Invalid password' |         error: 'Invalid password' | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var pw = '' |     let pw = '' | ||||||
|     if (newPassword) { |     if (newPassword) { | ||||||
|       pw = await this.hashPass(newPassword) |       pw = await this.hashPass(newPassword) | ||||||
|       if (!pw) { |       if (!pw) { | ||||||
| @ -211,7 +191,8 @@ class Auth { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     matchingUser.pash = pw |     matchingUser.pash = pw | ||||||
|     var success = await this.db.updateEntity('user', matchingUser) | 
 | ||||||
|  |     const success = await Database.updateUser(matchingUser) | ||||||
|     if (success) { |     if (success) { | ||||||
|       res.json({ |       res.json({ | ||||||
|         success: true |         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) { |   getLibraryItem(id) { | ||||||
|     return this.libraryItems.find(li => li.id === id) |     return this.libraryItems.find(li => li.id === id) | ||||||
|   } |   } | ||||||
|   getLibraryItemsInLibrary(libraryId) { |  | ||||||
|     return this.libraryItems.filter(li => li.libraryId === libraryId) |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   async updateLibraryItem(libraryItem) { |   async updateLibraryItem(libraryItem) { | ||||||
|     return this.updateLibraryItems([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) { |   removeLibraryItem(id) { | ||||||
|     return this.libraryItemsDb.delete((record) => record.id === id).then((results) => { |     return this.libraryItemsDb.delete((record) => record.id === id).then((results) => { | ||||||
|       Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`) |       Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`) | ||||||
| @ -303,14 +280,6 @@ class Db { | |||||||
|     return this.updateEntity('settings', this.serverSettings) |     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) { |   insertEntities(entityName, entities) { | ||||||
|     var entityDb = this.getEntityDb(entityName) |     var entityDb = this.getEntityDb(entityName) | ||||||
|     return entityDb.insert(entities).then((results) => { |     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) { |   getPlaybackSession(id) { | ||||||
|     return this.sessionsDb.select((pb) => pb.id == id).then((results) => { |     return this.sessionsDb.select((pb) => pb.id == id).then((results) => { | ||||||
|       if (results.data.length) { |       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
 |   // Check if server was updated and previous version was earlier than param
 | ||||||
|   checkPreviousVersionIsBefore(version) { |   checkPreviousVersionIsBefore(version) { | ||||||
|     if (!this.previousVersion) return false |     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') | const { version } = require('../package.json') | ||||||
| 
 | 
 | ||||||
| // Utils
 | // Utils
 | ||||||
| const dbMigration = require('./utils/dbMigration') |  | ||||||
| const filePerms = require('./utils/filePerms') | const filePerms = require('./utils/filePerms') | ||||||
| const fileUtils = require('./utils/fileUtils') | const fileUtils = require('./utils/fileUtils') | ||||||
| const globals = require('./utils/globals') |  | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
| 
 | 
 | ||||||
| const Auth = require('./Auth') | const Auth = require('./Auth') | ||||||
| const Watcher = require('./Watcher') | const Watcher = require('./Watcher') | ||||||
| const Scanner = require('./scanner/Scanner') | const Scanner = require('./scanner/Scanner') | ||||||
| const Db = require('./Db') | const Database = require('./Database') | ||||||
| const SocketAuthority = require('./SocketAuthority') | const SocketAuthority = require('./SocketAuthority') | ||||||
| 
 | 
 | ||||||
|  | const routes = require('./routes/index') | ||||||
|  | 
 | ||||||
| const ApiRouter = require('./routers/ApiRouter') | const ApiRouter = require('./routers/ApiRouter') | ||||||
| const HlsRouter = require('./routers/HlsRouter') | const HlsRouter = require('./routers/HlsRouter') | ||||||
| 
 | 
 | ||||||
| @ -29,7 +29,7 @@ const CoverManager = require('./managers/CoverManager') | |||||||
| const AbMergeManager = require('./managers/AbMergeManager') | const AbMergeManager = require('./managers/AbMergeManager') | ||||||
| const CacheManager = require('./managers/CacheManager') | const CacheManager = require('./managers/CacheManager') | ||||||
| const LogManager = require('./managers/LogManager') | const LogManager = require('./managers/LogManager') | ||||||
| const BackupManager = require('./managers/BackupManager') | // const BackupManager = require('./managers/BackupManager') // TODO
 | ||||||
| const PlaybackSessionManager = require('./managers/PlaybackSessionManager') | const PlaybackSessionManager = require('./managers/PlaybackSessionManager') | ||||||
| const PodcastManager = require('./managers/PodcastManager') | const PodcastManager = require('./managers/PodcastManager') | ||||||
| const AudioMetadataMangaer = require('./managers/AudioMetadataManager') | const AudioMetadataMangaer = require('./managers/AudioMetadataManager') | ||||||
| @ -59,30 +59,30 @@ class Server { | |||||||
|       filePerms.setDefaultDirSync(global.MetadataPath, false) |       filePerms.setDefaultDirSync(global.MetadataPath, false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.db = new Db() |     // this.db = new Db()
 | ||||||
|     this.watcher = new Watcher() |     this.watcher = new Watcher() | ||||||
|     this.auth = new Auth(this.db) |     this.auth = new Auth() | ||||||
| 
 | 
 | ||||||
|     // Managers
 |     // Managers
 | ||||||
|     this.taskManager = new TaskManager() |     this.taskManager = new TaskManager() | ||||||
|     this.notificationManager = new NotificationManager(this.db) |     this.notificationManager = new NotificationManager() | ||||||
|     this.emailManager = new EmailManager(this.db) |     this.emailManager = new EmailManager() | ||||||
|     this.backupManager = new BackupManager(this.db) |     // this.backupManager = new BackupManager(this.db)
 | ||||||
|     this.logManager = new LogManager(this.db) |     this.logManager = new LogManager() | ||||||
|     this.cacheManager = new CacheManager() |     this.cacheManager = new CacheManager() | ||||||
|     this.abMergeManager = new AbMergeManager(this.db, this.taskManager) |     this.abMergeManager = new AbMergeManager(this.taskManager) | ||||||
|     this.playbackSessionManager = new PlaybackSessionManager(this.db) |     this.playbackSessionManager = new PlaybackSessionManager() | ||||||
|     this.coverManager = new CoverManager(this.db, this.cacheManager) |     this.coverManager = new CoverManager(this.cacheManager) | ||||||
|     this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager) |     this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager) | ||||||
|     this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager) |     this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager) | ||||||
|     this.rssFeedManager = new RssFeedManager(this.db) |     this.rssFeedManager = new RssFeedManager() | ||||||
| 
 | 
 | ||||||
|     this.scanner = new Scanner(this.db, this.coverManager, this.taskManager) |     this.scanner = new Scanner(this.coverManager, this.taskManager) | ||||||
|     this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager) |     this.cronManager = new CronManager(this.scanner, this.podcastManager) | ||||||
| 
 | 
 | ||||||
|     // Routers
 |     // Routers
 | ||||||
|     this.apiRouter = new ApiRouter(this) |     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 |     Logger.logManager = this.logManager | ||||||
| 
 | 
 | ||||||
| @ -98,38 +98,28 @@ class Server { | |||||||
|     Logger.info('[Server] Init v' + version) |     Logger.info('[Server] Init v' + version) | ||||||
|     await this.playbackSessionManager.removeOrphanStreams() |     await this.playbackSessionManager.removeOrphanStreams() | ||||||
| 
 | 
 | ||||||
|     const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
 |     await Database.init(false) | ||||||
|     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() |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // Create token secret if does not exist (Added v2.1.0)
 |     // 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.auth.initTokenSecret() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.cleanUserData() // Remove invalid user item progress
 |     await this.cleanUserData() // Remove invalid user item progress
 | ||||||
|     await this.purgeMetadata() // Remove metadata folders without library item
 |     await this.purgeMetadata() // Remove metadata folders without library item
 | ||||||
|     await this.playbackSessionManager.removeInvalidSessions() |  | ||||||
|     await this.cacheManager.ensureCachePaths() |     await this.cacheManager.ensureCachePaths() | ||||||
| 
 | 
 | ||||||
|     await this.backupManager.init() |     // await this.backupManager.init() // TODO: Implement backups
 | ||||||
|     await this.logManager.init() |     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() |     await this.rssFeedManager.init() | ||||||
|     this.cronManager.init() |     this.cronManager.init() | ||||||
| 
 | 
 | ||||||
|     if (this.db.serverSettings.scannerDisableWatcher) { |     if (Database.serverSettings.scannerDisableWatcher) { | ||||||
|       Logger.info(`[Server] Watcher is disabled`) |       Logger.info(`[Server] Watcher is disabled`) | ||||||
|       this.watcher.disabled = true |       this.watcher.disabled = true | ||||||
|     } else { |     } else { | ||||||
|       this.watcher.initWatcher(this.db.libraries) |       this.watcher.initWatcher(Database.libraries) | ||||||
|       this.watcher.on('files', this.filesChanged.bind(this)) |       this.watcher.on('files', this.filesChanged.bind(this)) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -162,6 +152,7 @@ class Server { | |||||||
|     // Static folder
 |     // Static folder
 | ||||||
|     router.use(express.static(Path.join(global.appRoot, 'static'))) |     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('/api', this.authMiddleware.bind(this), this.apiRouter.router) | ||||||
|     router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.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('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) | ||||||
|     router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) |     router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) | ||||||
|     router.post('/init', (req, res) => { |     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`) |         Logger.error(`[Server] attempt to init server when server already has a root user`) | ||||||
|         return res.sendStatus(500) |         return res.sendStatus(500) | ||||||
|       } |       } | ||||||
| @ -213,8 +204,8 @@ class Server { | |||||||
|       // status check for client to see if server has been initialized
 |       // status check for client to see if server has been initialized
 | ||||||
|       // server has been initialized if a root user exists
 |       // server has been initialized if a root user exists
 | ||||||
|       const payload = { |       const payload = { | ||||||
|         isInit: this.db.hasRootUser, |         isInit: Database.hasRootUser, | ||||||
|         language: this.db.serverSettings.language |         language: Database.serverSettings.language | ||||||
|       } |       } | ||||||
|       if (!payload.isInit) { |       if (!payload.isInit) { | ||||||
|         payload.ConfigPath = global.ConfigPath |         payload.ConfigPath = global.ConfigPath | ||||||
| @ -243,7 +234,7 @@ class Server { | |||||||
|     let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : '' |     let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : '' | ||||||
|     if (!rootPash) Logger.warn(`[Server] Creating root user with no password`) |     if (!rootPash) Logger.warn(`[Server] Creating root user with no password`) | ||||||
|     let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username }) |     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) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| @ -261,7 +252,7 @@ class Server { | |||||||
| 
 | 
 | ||||||
|     let purged = 0 |     let purged = 0 | ||||||
|     await Promise.all(foldersInItemsMetadata.map(async foldername => { |     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) { |       if (!hasMatchingItem) { | ||||||
|         const folderPath = Path.join(itemsMetadata, foldername) |         const folderPath = Path.join(itemsMetadata, foldername) | ||||||
|         Logger.debug(`[Server] Purging unused metadata ${folderPath}`) |         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
 |   // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
 | ||||||
|   async cleanUserData() { |   async cleanUserData() { | ||||||
|     for (let i = 0; i < this.db.users.length; i++) { |     for (const _user of Database.users) { | ||||||
|       const _user = this.db.users[i] |  | ||||||
|       let hasUpdated = false |  | ||||||
|       if (_user.mediaProgress.length) { |       if (_user.mediaProgress.length) { | ||||||
|         const lengthBefore = _user.mediaProgress.length |         for (const mediaProgress of _user.mediaProgress) { | ||||||
|         _user.mediaProgress = _user.mediaProgress.filter(mp => { |           const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId) | ||||||
|           const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) |           if (libraryItem && mediaProgress.episodeId) { | ||||||
|           if (!libraryItem) return false |             const episode = libraryItem.media.checkHasEpisode?.(mediaProgress.episodeId) | ||||||
|           if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
 |             if (episode) continue | ||||||
|           return true |           } else { | ||||||
|         }) |             continue | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|         if (lengthBefore > _user.mediaProgress.length) { |           Logger.debug(`[Server] Removing media progress ${mediaProgress.id} data from user ${_user.username}`) | ||||||
|           Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`) |           await Database.removeMediaProgress(mediaProgress.id) | ||||||
|           hasUpdated = true |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       let hasUpdated = false | ||||||
|       if (_user.seriesHideFromContinueListening.length) { |       if (_user.seriesHideFromContinueListening.length) { | ||||||
|         _user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => { |         _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 |             hasUpdated = true | ||||||
|             return false |             return false | ||||||
|           } |           } | ||||||
| @ -308,7 +299,7 @@ class Server { | |||||||
|         }) |         }) | ||||||
|       } |       } | ||||||
|       if (hasUpdated) { |       if (hasUpdated) { | ||||||
|         await this.db.updateEntity('user', _user) |         await Database.updateUser(_user) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -321,8 +312,8 @@ class Server { | |||||||
| 
 | 
 | ||||||
|   getLoginRateLimiter() { |   getLoginRateLimiter() { | ||||||
|     return rateLimit({ |     return rateLimit({ | ||||||
|       windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes
 |       windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
 | ||||||
|       max: this.db.serverSettings.rateLimitLoginRequests, |       max: Database.serverSettings.rateLimitLoginRequests, | ||||||
|       skipSuccessfulRequests: true, |       skipSuccessfulRequests: true, | ||||||
|       onLimitReached: this.loginLimitReached |       onLimitReached: this.loginLimitReached | ||||||
|     }) |     }) | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const SocketIO = require('socket.io') | const SocketIO = require('socket.io') | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
|  | const Database = require('./Database') | ||||||
| 
 | 
 | ||||||
| class SocketAuthority { | class SocketAuthority { | ||||||
|   constructor() { |   constructor() { | ||||||
| @ -18,7 +19,7 @@ class SocketAuthority { | |||||||
|         onlineUsersMap[client.user.id].connections++ |         onlineUsersMap[client.user.id].connections++ | ||||||
|       } else { |       } else { | ||||||
|         onlineUsersMap[client.user.id] = { |         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 |           connections: 1 | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @ -107,7 +108,7 @@ class SocketAuthority { | |||||||
|           delete this.clients[socket.id] |           delete this.clients[socket.id] | ||||||
|         } else { |         } else { | ||||||
|           Logger.debug('[Server] User Offline ' + _client.user.username) |           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 |           const disconnectTime = Date.now() - _client.connected_at | ||||||
|           Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`) |           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}`) |     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
 |     // Update user lastSeen
 | ||||||
|     user.lastSeen = Date.now() |     user.lastSeen = Date.now() | ||||||
|     await this.Server.db.updateEntity('user', user) |     await Database.updateUser(user) | ||||||
| 
 | 
 | ||||||
|     const initialPayload = { |     const initialPayload = { | ||||||
|       userId: client.user.id, |       userId: client.user.id, | ||||||
| @ -186,7 +187,7 @@ class SocketAuthority { | |||||||
| 
 | 
 | ||||||
|       if (client.user) { |       if (client.user) { | ||||||
|         Logger.debug('[Server] User Offline ' + client.user.username) |         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 |       delete this.clients[socketId].user | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ const { createNewSortInstance } = require('../libs/fastSort') | |||||||
| 
 | 
 | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const { reqSupportsWebp } = require('../utils/index') | 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
 |     // Used on author landing page to include library items and items grouped in series
 | ||||||
|     if (include.includes('items')) { |     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 (libraryId && li.libraryId !== libraryId) return false | ||||||
|         if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
 |         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) |         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 |     const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name | ||||||
| 
 | 
 | ||||||
|     // Check if author name matches another author and merge the authors
 |     // 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) { |     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
 |       itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
 | ||||||
|         libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor) |         libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor) | ||||||
|  |         bookAuthorsToCreate.push({ | ||||||
|  |           bookId: libraryItem.media.id, | ||||||
|  |           authorId: existingAuthor.id | ||||||
|  |         }) | ||||||
|       }) |       }) | ||||||
|       if (itemsWithAuthor.length) { |       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())) |         SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded())) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // Remove old author
 |       // Remove old author
 | ||||||
|       await this.db.removeEntity('author', req.author.id) |       await Database.removeAuthor(req.author.id) | ||||||
|       SocketAuthority.emitter('author_removed', req.author.toJSON()) |       SocketAuthority.emitter('author_removed', req.author.toJSON()) | ||||||
| 
 | 
 | ||||||
|       // Send updated num books for merged author
 |       // 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) |         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id) | ||||||
|       }).length |       }).length | ||||||
|       SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) |       SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) | ||||||
| @ -131,18 +138,17 @@ class AuthorController { | |||||||
|         req.author.updatedAt = Date.now() |         req.author.updatedAt = Date.now() | ||||||
| 
 | 
 | ||||||
|         if (authorNameUpdate) { // Update author name on all books
 |         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 => { |           itemsWithAuthor.forEach(libraryItem => { | ||||||
|             libraryItem.media.metadata.updateAuthor(req.author) |             libraryItem.media.metadata.updateAuthor(req.author) | ||||||
|           }) |           }) | ||||||
|           if (itemsWithAuthor.length) { |           if (itemsWithAuthor.length) { | ||||||
|             await this.db.updateLibraryItems(itemsWithAuthor) |  | ||||||
|             SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded())) |             SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded())) | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         await this.db.updateEntity('author', req.author) |         await Database.updateAuthor(req.author) | ||||||
|         const numBooks = this.db.libraryItems.filter(li => { |         const numBooks = Database.libraryItems.filter(li => { | ||||||
|           return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) |           return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) | ||||||
|         }).length |         }).length | ||||||
|         SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) |         SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||||
| @ -159,7 +165,7 @@ class AuthorController { | |||||||
|     var q = (req.query.q || '').toLowerCase() |     var q = (req.query.q || '').toLowerCase() | ||||||
|     if (!q) return res.json([]) |     if (!q) return res.json([]) | ||||||
|     var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25 |     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) |     authors = authors.slice(0, limit) | ||||||
|     res.json({ |     res.json({ | ||||||
|       results: authors |       results: authors | ||||||
| @ -204,8 +210,8 @@ class AuthorController { | |||||||
|     if (hasUpdates) { |     if (hasUpdates) { | ||||||
|       req.author.updatedAt = Date.now() |       req.author.updatedAt = Date.now() | ||||||
| 
 | 
 | ||||||
|       await this.db.updateEntity('author', req.author) |       await Database.updateAuthor(req.author) | ||||||
|       const numBooks = this.db.libraryItems.filter(li => { |       const numBooks = Database.libraryItems.filter(li => { | ||||||
|         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) |         return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id) | ||||||
|       }).length |       }).length | ||||||
|       SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) |       SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||||
| @ -238,7 +244,7 @@ class AuthorController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   middleware(req, res, next) { |   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 (!author) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { |     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const Collection = require('../objects/Collection') | const Collection = require('../objects/Collection') | ||||||
| 
 | 
 | ||||||
| @ -13,22 +14,22 @@ class CollectionController { | |||||||
|     if (!success) { |     if (!success) { | ||||||
|       return res.status(500).send('Invalid collection data') |       return res.status(500).send('Invalid collection data') | ||||||
|     } |     } | ||||||
|     var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems) |     var jsonExpanded = newCollection.toJSONExpanded(Database.libraryItems) | ||||||
|     await this.db.insertEntity('collection', newCollection) |     await Database.createCollection(newCollection) | ||||||
|     SocketAuthority.emitter('collection_added', jsonExpanded) |     SocketAuthority.emitter('collection_added', jsonExpanded) | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   findAll(req, res) { |   findAll(req, res) { | ||||||
|     res.json({ |     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) { |   findOne(req, res) { | ||||||
|     const includeEntities = (req.query.include || '').split(',') |     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')) { |     if (includeEntities.includes('rssfeed')) { | ||||||
|       const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id) |       const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id) | ||||||
| @ -41,9 +42,9 @@ class CollectionController { | |||||||
|   async update(req, res) { |   async update(req, res) { | ||||||
|     const collection = req.collection |     const collection = req.collection | ||||||
|     const wasUpdated = collection.update(req.body) |     const wasUpdated = collection.update(req.body) | ||||||
|     const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||||
|     if (wasUpdated) { |     if (wasUpdated) { | ||||||
|       await this.db.updateEntity('collection', collection) |       await Database.updateCollection(collection) | ||||||
|       SocketAuthority.emitter('collection_updated', jsonExpanded) |       SocketAuthority.emitter('collection_updated', jsonExpanded) | ||||||
|     } |     } | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
| @ -51,19 +52,19 @@ class CollectionController { | |||||||
| 
 | 
 | ||||||
|   async delete(req, res) { |   async delete(req, res) { | ||||||
|     const collection = req.collection |     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
 |     // Close rss feed - remove from db and emit socket event
 | ||||||
|     await this.rssFeedManager.closeFeedForEntityId(collection.id) |     await this.rssFeedManager.closeFeedForEntityId(collection.id) | ||||||
| 
 | 
 | ||||||
|     await this.db.removeEntity('collection', collection.id) |     await Database.removeCollection(collection.id) | ||||||
|     SocketAuthority.emitter('collection_removed', jsonExpanded) |     SocketAuthority.emitter('collection_removed', jsonExpanded) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async addBook(req, res) { |   async addBook(req, res) { | ||||||
|     const collection = req.collection |     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) { |     if (!libraryItem) { | ||||||
|       return res.status(500).send('Book not found') |       return res.status(500).send('Book not found') | ||||||
|     } |     } | ||||||
| @ -74,8 +75,14 @@ class CollectionController { | |||||||
|       return res.status(500).send('Book already in collection') |       return res.status(500).send('Book already in collection') | ||||||
|     } |     } | ||||||
|     collection.addBook(req.body.id) |     collection.addBook(req.body.id) | ||||||
|     const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||||
|     await this.db.updateEntity('collection', collection) | 
 | ||||||
|  |     const collectionBook = { | ||||||
|  |       collectionId: collection.id, | ||||||
|  |       bookId: libraryItem.media.id, | ||||||
|  |       order: collection.books.length | ||||||
|  |     } | ||||||
|  |     await Database.createCollectionBook(collectionBook) | ||||||
|     SocketAuthority.emitter('collection_updated', jsonExpanded) |     SocketAuthority.emitter('collection_updated', jsonExpanded) | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
|   } |   } | ||||||
| @ -83,13 +90,18 @@ class CollectionController { | |||||||
|   // DELETE: api/collections/:id/book/:bookId
 |   // DELETE: api/collections/:id/book/:bookId
 | ||||||
|   async removeBook(req, res) { |   async removeBook(req, res) { | ||||||
|     const collection = req.collection |     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)) { |     if (collection.books.includes(req.params.bookId)) { | ||||||
|       collection.removeBook(req.params.bookId) |       collection.removeBook(req.params.bookId) | ||||||
|       var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems) |       const jsonExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||||
|       await this.db.updateEntity('collection', collection) |  | ||||||
|       SocketAuthority.emitter('collection_updated', jsonExpanded) |       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
 |   // POST: api/collections/:id/batch/add
 | ||||||
| @ -98,19 +110,30 @@ class CollectionController { | |||||||
|     if (!req.body.books || !req.body.books.length) { |     if (!req.body.books || !req.body.books.length) { | ||||||
|       return res.status(500).send('Invalid request body') |       return res.status(500).send('Invalid request body') | ||||||
|     } |     } | ||||||
|     var bookIdsToAdd = req.body.books |     const bookIdsToAdd = req.body.books | ||||||
|     var hasUpdated = false |     const collectionBooksToAdd = [] | ||||||
|     for (let i = 0; i < bookIdsToAdd.length; i++) { |     let hasUpdated = false | ||||||
|       if (!collection.books.includes(bookIdsToAdd[i])) { | 
 | ||||||
|         collection.addBook(bookIdsToAdd[i]) |     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 |         hasUpdated = true | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('collection', collection) |       await Database.createBulkCollectionBooks(collectionBooksToAdd) | ||||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) |       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
 |   // POST: api/collections/:id/batch/remove
 | ||||||
| @ -120,23 +143,26 @@ class CollectionController { | |||||||
|       return res.status(500).send('Invalid request body') |       return res.status(500).send('Invalid request body') | ||||||
|     } |     } | ||||||
|     var bookIdsToRemove = req.body.books |     var bookIdsToRemove = req.body.books | ||||||
|     var hasUpdated = false |     let hasUpdated = false | ||||||
|     for (let i = 0; i < bookIdsToRemove.length; i++) { |     for (const libraryItemId of bookIdsToRemove) { | ||||||
|       if (collection.books.includes(bookIdsToRemove[i])) { |       const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId) | ||||||
|         collection.removeBook(bookIdsToRemove[i]) |       if (!libraryItem) continue | ||||||
|  | 
 | ||||||
|  |       if (collection.books.includes(libraryItemId)) { | ||||||
|  |         collection.removeBook(libraryItemId) | ||||||
|         hasUpdated = true |         hasUpdated = true | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('collection', collection) |       await Database.updateCollection(collection) | ||||||
|       SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) |       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) { |   middleware(req, res, next) { | ||||||
|     if (req.params.id) { |     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) { |       if (!collection) { | ||||||
|         return res.status(404).send('Collection not found') |         return res.status(404).send('Collection not found') | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,22 +1,23 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| class EmailController { | class EmailController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| 
 | 
 | ||||||
|   getSettings(req, res) { |   getSettings(req, res) { | ||||||
|     res.json({ |     res.json({ | ||||||
|       settings: this.db.emailSettings |       settings: Database.emailSettings | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async updateSettings(req, res) { |   async updateSettings(req, res) { | ||||||
|     const updated = this.db.emailSettings.update(req.body) |     const updated = Database.emailSettings.update(req.body) | ||||||
|     if (updated) { |     if (updated) { | ||||||
|       await this.db.updateEntity('settings', this.db.emailSettings) |       await Database.updateSetting(Database.emailSettings) | ||||||
|     } |     } | ||||||
|     res.json({ |     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 |       ereaderDevices | ||||||
|     }) |     }) | ||||||
|     if (updated) { |     if (updated) { | ||||||
|       await this.db.updateEntity('settings', this.db.emailSettings) |       await Database.updateSetting(Database.emailSettings) | ||||||
|       SocketAuthority.adminEmitter('ereader-devices-updated', { |       SocketAuthority.adminEmitter('ereader-devices-updated', { | ||||||
|         ereaderDevices: this.db.emailSettings.ereaderDevices |         ereaderDevices: Database.emailSettings.ereaderDevices | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|     res.json({ |     res.json({ | ||||||
|       ereaderDevices: this.db.emailSettings.ereaderDevices |       ereaderDevices: Database.emailSettings.ereaderDevices | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async sendEBookToDevice(req, res) { |   async sendEBookToDevice(req, res) { | ||||||
|     Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) |     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) { |     if (!libraryItem) { | ||||||
|       return res.status(404).send('Library item not found') |       return res.status(404).send('Library item not found') | ||||||
|     } |     } | ||||||
| @ -67,7 +68,7 @@ class EmailController { | |||||||
|       return res.status(404).send('EBook file not found') |       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) { |     if (!device) { | ||||||
|       return res.status(404).send('E-reader device not found') |       return res.status(404).send('E-reader device not found') | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| 
 | 
 | ||||||
| class FileSystemController { | class FileSystemController { | ||||||
| @ -16,7 +17,7 @@ class FileSystemController { | |||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     // Do not include existing mapped library paths in response
 |     // Do not include existing mapped library paths in response
 | ||||||
|     this.db.libraries.forEach(lib => { |     Database.libraries.forEach(lib => { | ||||||
|       lib.folders.forEach((folder) => { |       lib.folders.forEach((folder) => { | ||||||
|         let dir = folder.fullPath |         let dir = folder.fullPath | ||||||
|         if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') |         if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '') | ||||||
|  | |||||||
| @ -9,6 +9,9 @@ const { sort, createNewSortInstance } = require('../libs/fastSort') | |||||||
| const naturalSort = createNewSortInstance({ | const naturalSort = createNewSortInstance({ | ||||||
|   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare |   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare | ||||||
| }) | }) | ||||||
|  | 
 | ||||||
|  | const Database = require('../Database') | ||||||
|  | 
 | ||||||
| class LibraryController { | class LibraryController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| 
 | 
 | ||||||
| @ -40,13 +43,13 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const library = new Library() |     const library = new Library() | ||||||
|     newLibraryPayload.displayOrder = this.db.libraries.length + 1 |     newLibraryPayload.displayOrder = Database.libraries.length + 1 | ||||||
|     library.setData(newLibraryPayload) |     library.setData(newLibraryPayload) | ||||||
|     await this.db.insertEntity('library', library) |     await Database.createLibrary(library) | ||||||
| 
 | 
 | ||||||
|     // Only emit to users with access to library
 |     // Only emit to users with access to library
 | ||||||
|     const userFilter = (user) => { |     const userFilter = (user) => { | ||||||
|       return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id) |       return user.checkCanAccessLibrary?.(library.id) | ||||||
|     } |     } | ||||||
|     SocketAuthority.emitter('library_added', library.toJSON(), userFilter) |     SocketAuthority.emitter('library_added', library.toJSON(), userFilter) | ||||||
| 
 | 
 | ||||||
| @ -58,14 +61,15 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|   findAll(req, res) { |   findAll(req, res) { | ||||||
|     const librariesAccessible = req.user.librariesAccessible || [] |     const librariesAccessible = req.user.librariesAccessible || [] | ||||||
|     if (librariesAccessible && librariesAccessible.length) { |     if (librariesAccessible.length) { | ||||||
|       return res.json({ |       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({ |     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({ |       return res.json({ | ||||||
|         filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems), |         filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems), | ||||||
|         issues: req.libraryItems.filter(li => li.hasIssues).length, |         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 |         library: req.library | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| @ -128,14 +132,14 @@ class LibraryController { | |||||||
|       this.cronManager.updateLibraryScanCron(library) |       this.cronManager.updateLibraryScanCron(library) | ||||||
| 
 | 
 | ||||||
|       // Remove libraryItems no longer in 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) { |       if (itemsToRemove.length) { | ||||||
|         Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`) |         Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`) | ||||||
|         for (let i = 0; i < itemsToRemove.length; i++) { |         for (let i = 0; i < itemsToRemove.length; i++) { | ||||||
|           await this.handleDeleteLibraryItem(itemsToRemove[i]) |           await this.handleDeleteLibraryItem(itemsToRemove[i]) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       await this.db.updateEntity('library', library) |       await Database.updateLibrary(library) | ||||||
| 
 | 
 | ||||||
|       // Only emit to users with access to library
 |       // Only emit to users with access to library
 | ||||||
|       const userFilter = (user) => { |       const userFilter = (user) => { | ||||||
| @ -153,21 +157,21 @@ class LibraryController { | |||||||
|     this.watcher.removeLibrary(library) |     this.watcher.removeLibrary(library) | ||||||
| 
 | 
 | ||||||
|     // Remove collections for 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) { |     for (const collection of collections) { | ||||||
|       Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`) |       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
 |     // 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"`) |     Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`) | ||||||
|     for (let i = 0; i < libraryItems.length; i++) { |     for (let i = 0; i < libraryItems.length; i++) { | ||||||
|       await this.handleDeleteLibraryItem(libraryItems[i]) |       await this.handleDeleteLibraryItem(libraryItems[i]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const libraryJson = library.toJSON() |     const libraryJson = library.toJSON() | ||||||
|     await this.db.removeEntity('library', library.id) |     await Database.removeLibrary(library.id) | ||||||
|     SocketAuthority.emitter('library_removed', libraryJson) |     SocketAuthority.emitter('library_removed', libraryJson) | ||||||
|     return res.json(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
 |     // 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.
 |     // to series having a collapsed series that is just that series.
 | ||||||
|     if (payload.collapseseries) { |     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)) { |       if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { | ||||||
|         libraryItems = collapsedItems |         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)
 |       // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
 | ||||||
|       sortArray.push({ |       sortArray.push({ | ||||||
|         asc: (li) => { |         asc: (li) => { | ||||||
|           if (this.db.serverSettings.sortingIgnorePrefix) { |           if (Database.serverSettings.sortingIgnorePrefix) { | ||||||
|             return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix |             return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix | ||||||
|           } else { |           } else { | ||||||
|             return li.collapsedSeries?.name || li.media.metadata.title |             return li.collapsedSeries?.name || li.media.metadata.title | ||||||
| @ -255,7 +259,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|       // Handle server setting sortingIgnorePrefix
 |       // Handle server setting sortingIgnorePrefix
 | ||||||
|       const sortByTitle = sortKey === 'media.metadata.title' |       const sortByTitle = sortKey === 'media.metadata.title' | ||||||
|       if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) { |       if (sortByTitle && Database.serverSettings.sortingIgnorePrefix) { | ||||||
|         // BookMetadata.js has titleIgnorePrefix getter
 |         // BookMetadata.js has titleIgnorePrefix getter
 | ||||||
|         sortKey += 'IgnorePrefix' |         sortKey += 'IgnorePrefix' | ||||||
|       } |       } | ||||||
| @ -267,7 +271,7 @@ class LibraryController { | |||||||
|         sortArray.push({ |         sortArray.push({ | ||||||
|           asc: (li) => { |           asc: (li) => { | ||||||
|             if (li.collapsedSeries) { |             if (li.collapsedSeries) { | ||||||
|               return this.db.serverSettings.sortingIgnorePrefix ? |               return Database.serverSettings.sortingIgnorePrefix ? | ||||||
|                 li.collapsedSeries.nameIgnorePrefix : |                 li.collapsedSeries.nameIgnorePrefix : | ||||||
|                 li.collapsedSeries.name |                 li.collapsedSeries.name | ||||||
|             } else { |             } else { | ||||||
| @ -284,7 +288,7 @@ class LibraryController { | |||||||
|           if (mediaIsBook && sortBySequence) { |           if (mediaIsBook && sortBySequence) { | ||||||
|             return li.media.metadata.getSeries(filterSeries).sequence |             return li.media.metadata.getSeries(filterSeries).sequence | ||||||
|           } else if (mediaIsBook && sortByTitle && li.collapsedSeries) { |           } else if (mediaIsBook && sortByTitle && li.collapsedSeries) { | ||||||
|             return this.db.serverSettings.sortingIgnorePrefix ? |             return Database.serverSettings.sortingIgnorePrefix ? | ||||||
|               li.collapsedSeries.nameIgnorePrefix : |               li.collapsedSeries.nameIgnorePrefix : | ||||||
|               li.collapsedSeries.name |               li.collapsedSeries.name | ||||||
|           } else { |           } else { | ||||||
| @ -405,7 +409,7 @@ class LibraryController { | |||||||
|       include: include.join(',') |       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' |     const direction = payload.sortDesc ? 'desc' : 'asc' | ||||||
|     series = naturalSort(series).by([ |     series = naturalSort(series).by([ | ||||||
| @ -422,7 +426,7 @@ class LibraryController { | |||||||
|           } else if (payload.sortBy === 'lastBookAdded') { |           } else if (payload.sortBy === 'lastBookAdded') { | ||||||
|             return Math.max(...(se.books).map(x => x.addedAt), 0) |             return Math.max(...(se.books).map(x => x.addedAt), 0) | ||||||
|           } else { // sort by name
 |           } 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(',') |       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) |       const expanded = c.toJSONExpanded(libraryItems, payload.minified) | ||||||
| 
 | 
 | ||||||
|       // If all books restricted to user in this collection then hide this collection
 |       // If all books restricted to user in this collection then hide this collection
 | ||||||
| @ -493,7 +497,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|   // api/libraries/:id/playlists
 |   // api/libraries/:id/playlists
 | ||||||
|   async getUserPlaylistsForLibrary(req, res) { |   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 = { |     const payload = { | ||||||
|       results: [], |       results: [], | ||||||
| @ -517,7 +521,7 @@ class LibraryController { | |||||||
|       return res.status(400).send('Invalid library media type') |       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) |     let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems) | ||||||
|     albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
 |     albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
 | ||||||
| 
 | 
 | ||||||
| @ -561,26 +565,26 @@ class LibraryController { | |||||||
|     var orderdata = req.body |     var orderdata = req.body | ||||||
|     var hasUpdates = false |     var hasUpdates = false | ||||||
|     for (let i = 0; i < orderdata.length; i++) { |     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) { |       if (!library) { | ||||||
|         Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) |         Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) | ||||||
|         return res.sendStatus(500) |         return res.sendStatus(500) | ||||||
|       } |       } | ||||||
|       if (library.update({ displayOrder: orderdata[i].newOrder })) { |       if (library.update({ displayOrder: orderdata[i].newOrder })) { | ||||||
|         hasUpdates = true |         hasUpdates = true | ||||||
|         await this.db.updateEntity('library', library) |         await Database.updateLibrary(library) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (hasUpdates) { |     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`) |       Logger.debug(`[LibraryController] Updated library display orders`) | ||||||
|     } else { |     } else { | ||||||
|       Logger.debug(`[LibraryController] Library orders were up to date`) |       Logger.debug(`[LibraryController] Library orders were up to date`) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     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) { |       if (queryResult.series?.length) { | ||||||
|         queryResult.series.forEach((se) => { |         queryResult.series.forEach((se) => { | ||||||
|           if (!seriesMatches[se.id]) { |           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()] } |             if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] } | ||||||
|           } else { |           } else { | ||||||
|             seriesMatches[se.id].books.push(li.toJSON()) |             seriesMatches[se.id].books.push(li.toJSON()) | ||||||
| @ -620,7 +624,7 @@ class LibraryController { | |||||||
|       if (queryResult.authors?.length) { |       if (queryResult.authors?.length) { | ||||||
|         queryResult.authors.forEach((au) => { |         queryResult.authors.forEach((au) => { | ||||||
|           if (!authorMatches[au.id]) { |           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) { |             if (_author) { | ||||||
|               authorMatches[au.id] = _author.toJSON() |               authorMatches[au.id] = _author.toJSON() | ||||||
|               authorMatches[au.id].numBooks = 1 |               authorMatches[au.id].numBooks = 1 | ||||||
| @ -687,7 +691,7 @@ class LibraryController { | |||||||
|       if (li.media.metadata.authors && li.media.metadata.authors.length) { |       if (li.media.metadata.authors && li.media.metadata.authors.length) { | ||||||
|         li.media.metadata.authors.forEach((au) => { |         li.media.metadata.authors.forEach((au) => { | ||||||
|           if (!authors[au.id]) { |           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) { |             if (_author) { | ||||||
|               authors[au.id] = _author.toJSON() |               authors[au.id] = _author.toJSON() | ||||||
|               authors[au.id].numBooks = 1 |               authors[au.id].numBooks = 1 | ||||||
| @ -749,7 +753,7 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (itemsUpdated.length) { |     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())) |       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -774,7 +778,7 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (itemsUpdated.length) { |     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())) |       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -858,12 +862,12 @@ class LibraryController { | |||||||
|       return res.sendStatus(404) |       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) { |     if (!library) { | ||||||
|       return res.status(404).send('Library not found') |       return res.status(404).send('Library not found') | ||||||
|     } |     } | ||||||
|     req.library = library |     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) |       return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li) | ||||||
|     }) |     }) | ||||||
|     next() |     next() | ||||||
|  | |||||||
| @ -2,9 +2,10 @@ const Path = require('path') | |||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const zipHelpers = require('../utils/zipHelpers') | const zipHelpers = require('../utils/zipHelpers') | ||||||
| const { reqSupportsWebp, isNullOrNaN } = require('../utils/index') | const { reqSupportsWebp } = require('../utils/index') | ||||||
| const { ScanResult } = require('../utils/constants') | const { ScanResult } = require('../utils/constants') | ||||||
| const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') | const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') | ||||||
| 
 | 
 | ||||||
| @ -31,7 +32,7 @@ class LibraryItemController { | |||||||
|       if (item.mediaType == 'book') { |       if (item.mediaType == 'book') { | ||||||
|         if (includeEntities.includes('authors')) { |         if (includeEntities.includes('authors')) { | ||||||
|           item.media.metadata.authors = item.media.metadata.authors.map(au => { |           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 |             if (!author) return null | ||||||
|             return { |             return { | ||||||
|               ...author |               ...author | ||||||
| @ -61,7 +62,7 @@ class LibraryItemController { | |||||||
|     const hasUpdates = libraryItem.update(req.body) |     const hasUpdates = libraryItem.update(req.body) | ||||||
|     if (hasUpdates) { |     if (hasUpdates) { | ||||||
|       Logger.debug(`[LibraryItemController] Updated now saving`) |       Logger.debug(`[LibraryItemController] Updated now saving`) | ||||||
|       await this.db.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
|     res.json(libraryItem.toJSON()) |     res.json(libraryItem.toJSON()) | ||||||
| @ -139,7 +140,7 @@ class LibraryItemController { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) |       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()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
|     res.json({ |     res.json({ | ||||||
| @ -174,7 +175,7 @@ class LibraryItemController { | |||||||
|       return res.status(500).send('Unknown error occurred') |       return res.status(500).send('Unknown error occurred') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     res.json({ |     res.json({ | ||||||
|       success: true, |       success: true, | ||||||
| @ -194,7 +195,7 @@ class LibraryItemController { | |||||||
|       return res.status(500).send(validationResult.error) |       return res.status(500).send(validationResult.error) | ||||||
|     } |     } | ||||||
|     if (validationResult.updated) { |     if (validationResult.updated) { | ||||||
|       await this.db.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
|     res.json({ |     res.json({ | ||||||
| @ -210,7 +211,7 @@ class LibraryItemController { | |||||||
|     if (libraryItem.media.coverPath) { |     if (libraryItem.media.coverPath) { | ||||||
|       libraryItem.updateMediaCover('') |       libraryItem.updateMediaCover('') | ||||||
|       await this.cacheManager.purgeCoverCache(libraryItem.id) |       await this.cacheManager.purgeCoverCache(libraryItem.id) | ||||||
|       await this.db.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -282,7 +283,7 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(500) |       return res.sendStatus(500) | ||||||
|     } |     } | ||||||
|     libraryItem.media.updateAudioTracks(orderedFileData) |     libraryItem.media.updateAudioTracks(orderedFileData) | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     res.json(libraryItem.toJSON()) |     res.json(libraryItem.toJSON()) | ||||||
|   } |   } | ||||||
| @ -309,7 +310,7 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(500) |       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) { |     if (!itemsToDelete.length) { | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
| @ -338,7 +339,7 @@ class LibraryItemController { | |||||||
| 
 | 
 | ||||||
|     for (let i = 0; i < updatePayloads.length; i++) { |     for (let i = 0; i < updatePayloads.length; i++) { | ||||||
|       var mediaPayload = updatePayloads[i].mediaPayload |       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 |       if (!libraryItem) return null | ||||||
| 
 | 
 | ||||||
|       await this.createAuthorsAndSeriesForItemUpdate(mediaPayload) |       await this.createAuthorsAndSeriesForItemUpdate(mediaPayload) | ||||||
| @ -346,7 +347,7 @@ class LibraryItemController { | |||||||
|       var hasUpdates = libraryItem.media.update(mediaPayload) |       var hasUpdates = libraryItem.media.update(mediaPayload) | ||||||
|       if (hasUpdates) { |       if (hasUpdates) { | ||||||
|         Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) |         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()) |         SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|         itemsUpdated++ |         itemsUpdated++ | ||||||
|       } |       } | ||||||
| @ -366,7 +367,7 @@ class LibraryItemController { | |||||||
|     } |     } | ||||||
|     const libraryItems = [] |     const libraryItems = [] | ||||||
|     libraryItemIds.forEach((lid) => { |     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()) |       if (li) libraryItems.push(li.toJSONExpanded()) | ||||||
|     }) |     }) | ||||||
|     res.json({ |     res.json({ | ||||||
| @ -389,7 +390,7 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(400) |       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) { |     if (!libraryItems?.length) { | ||||||
|       return res.sendStatus(400) |       return res.sendStatus(400) | ||||||
|     } |     } | ||||||
| @ -424,7 +425,7 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(400) |       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) { |     if (!libraryItems?.length) { | ||||||
|       return res.sendStatus(400) |       return res.sendStatus(400) | ||||||
|     } |     } | ||||||
| @ -441,15 +442,17 @@ class LibraryItemController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // DELETE: api/items/all
 |   // DELETE: api/items/all
 | ||||||
|  |   // TODO: Remove
 | ||||||
|   async deleteAll(req, res) { |   async deleteAll(req, res) { | ||||||
|     if (!req.user.isAdminOrUp) { |     return res.sendStatus(404) | ||||||
|       Logger.warn('User other than admin attempted to delete all library items', req.user) |     // if (!req.user.isAdminOrUp) {
 | ||||||
|       return res.sendStatus(403) |     //   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() |     // Logger.info('Removing all Library Items')
 | ||||||
|     if (success) res.sendStatus(200) |     // var success = await this.db.recreateLibraryItemsDb()
 | ||||||
|     else res.sendStatus(500) |     // if (success) res.sendStatus(200)
 | ||||||
|  |     // else res.sendStatus(500)
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // POST: api/items/:id/scan (admin)
 |   // POST: api/items/:id/scan (admin)
 | ||||||
| @ -504,7 +507,7 @@ class LibraryItemController { | |||||||
|     const chapters = req.body.chapters || [] |     const chapters = req.body.chapters || [] | ||||||
|     const wasUpdated = req.libraryItem.media.updateChapters(chapters) |     const wasUpdated = req.libraryItem.media.updateChapters(chapters) | ||||||
|     if (wasUpdated) { |     if (wasUpdated) { | ||||||
|       await this.db.updateLibraryItem(req.libraryItem) |       await Database.updateLibraryItem(req.libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -586,7 +589,7 @@ class LibraryItemController { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     req.libraryItem.updatedAt = Date.now() |     req.libraryItem.updatedAt = Date.now() | ||||||
|     await this.db.updateLibraryItem(req.libraryItem) |     await Database.updateLibraryItem(req.libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| @ -682,13 +685,13 @@ class LibraryItemController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     req.libraryItem.updatedAt = Date.now() |     req.libraryItem.updatedAt = Date.now() | ||||||
|     await this.db.updateLibraryItem(req.libraryItem) |     await Database.updateLibraryItem(req.libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   middleware(req, res, next) { |   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) |     if (!req.libraryItem?.media) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     // Check user can access this library item
 |     // Check user can access this library item
 | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| const { sort } = require('../libs/fastSort') | const { sort } = require('../libs/fastSort') | ||||||
| const { isObject, toNumber } = require('../utils/index') | const { toNumber } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
| class MeController { | class MeController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -33,7 +34,7 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|   // GET: api/me/listening-stats
 |   // GET: api/me/listening-stats
 | ||||||
|   async getListeningStats(req, res) { |   async getListeningStats(req, res) { | ||||||
|     var listeningStats = await this.getUserListeningStatsHelpers(req.user.id) |     const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) | ||||||
|     res.json(listeningStats) |     res.json(listeningStats) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -51,21 +52,21 @@ class MeController { | |||||||
|     if (!req.user.removeMediaProgress(req.params.id)) { |     if (!req.user.removeMediaProgress(req.params.id)) { | ||||||
|       return res.sendStatus(200) |       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()) |     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // PATCH: api/me/progress/:id
 |   // PATCH: api/me/progress/:id
 | ||||||
|   async createUpdateMediaProgress(req, res) { |   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) { |     if (!libraryItem) { | ||||||
|       return res.status(404).send('Item not found') |       return res.status(404).send('Item not found') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body) |     if (req.user.createUpdateMediaProgress(libraryItem, req.body)) { | ||||||
|     if (wasUpdated) { |       const mediaProgress = req.user.getMediaProgress(libraryItem.id) | ||||||
|       await this.db.updateEntity('user', req.user) |       if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
| @ -73,8 +74,8 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|   // PATCH: api/me/progress/:id/:episodeId
 |   // PATCH: api/me/progress/:id/:episodeId
 | ||||||
|   async createUpdateEpisodeMediaProgress(req, res) { |   async createUpdateEpisodeMediaProgress(req, res) { | ||||||
|     var episodeId = req.params.episodeId |     const episodeId = req.params.episodeId | ||||||
|     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) { |     if (!libraryItem) { | ||||||
|       return res.status(404).send('Item not found') |       return res.status(404).send('Item not found') | ||||||
|     } |     } | ||||||
| @ -83,9 +84,9 @@ class MeController { | |||||||
|       return res.status(404).send('Episode not found') |       return res.status(404).send('Episode not found') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId) |     if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) { | ||||||
|     if (wasUpdated) { |       const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId) | ||||||
|       await this.db.updateEntity('user', req.user) |       if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
| @ -93,24 +94,26 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|   // PATCH: api/me/progress/batch/update
 |   // PATCH: api/me/progress/batch/update
 | ||||||
|   async batchUpdateMediaProgress(req, res) { |   async batchUpdateMediaProgress(req, res) { | ||||||
|     var itemProgressPayloads = req.body |     const itemProgressPayloads = req.body | ||||||
|     if (!itemProgressPayloads || !itemProgressPayloads.length) { |     if (!itemProgressPayloads?.length) { | ||||||
|       return res.status(400).send('Missing request payload') |       return res.status(400).send('Missing request payload') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var shouldUpdate = false |     let shouldUpdate = false | ||||||
|     itemProgressPayloads.forEach((itemProgress) => { |     for (const itemProgress of itemProgressPayloads) { | ||||||
|       var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
 |       const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
 | ||||||
|       if (libraryItem) { |       if (libraryItem) { | ||||||
|         var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId) |         if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) { | ||||||
|         if (wasUpdated) shouldUpdate = true |           const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId) | ||||||
|  |           if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||||
|  |           shouldUpdate = true | ||||||
|  |         } | ||||||
|       } else { |       } else { | ||||||
|         Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`) |         Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`) | ||||||
|       } |       } | ||||||
|     }) |     } | ||||||
| 
 | 
 | ||||||
|     if (shouldUpdate) { |     if (shouldUpdate) { | ||||||
|       await this.db.updateEntity('user', req.user) |  | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -119,18 +122,18 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|   // POST: api/me/item/:id/bookmark
 |   // POST: api/me/item/:id/bookmark
 | ||||||
|   async createBookmark(req, res) { |   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) |     if (!libraryItem) return res.sendStatus(404) | ||||||
|     const { time, title } = req.body |     const { time, title } = req.body | ||||||
|     var bookmark = req.user.createBookmark(libraryItem.id, time, title) |     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()) |     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     res.json(bookmark) |     res.json(bookmark) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // PATCH: api/me/item/:id/bookmark
 |   // PATCH: api/me/item/:id/bookmark
 | ||||||
|   async updateBookmark(req, res) { |   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) |     if (!libraryItem) return res.sendStatus(404) | ||||||
|     const { time, title } = req.body |     const { time, title } = req.body | ||||||
|     if (!req.user.findBookmark(libraryItem.id, time)) { |     if (!req.user.findBookmark(libraryItem.id, time)) { | ||||||
| @ -139,14 +142,14 @@ class MeController { | |||||||
|     } |     } | ||||||
|     var bookmark = req.user.updateBookmark(libraryItem.id, time, title) |     var bookmark = req.user.updateBookmark(libraryItem.id, time, title) | ||||||
|     if (!bookmark) return res.sendStatus(500) |     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()) |     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     res.json(bookmark) |     res.json(bookmark) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // DELETE: api/me/item/:id/bookmark/:time
 |   // DELETE: api/me/item/:id/bookmark/:time
 | ||||||
|   async removeBookmark(req, res) { |   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) |     if (!libraryItem) return res.sendStatus(404) | ||||||
|     var time = Number(req.params.time) |     var time = Number(req.params.time) | ||||||
|     if (isNaN(time)) return res.sendStatus(500) |     if (isNaN(time)) return res.sendStatus(500) | ||||||
| @ -156,7 +159,7 @@ class MeController { | |||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
|     req.user.removeBookmark(libraryItem.id, time) |     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()) |     SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| @ -178,16 +181,16 @@ class MeController { | |||||||
|       return res.sendStatus(500) |       return res.sendStatus(500) | ||||||
|     } |     } | ||||||
|     const updatedLocalMediaProgress = [] |     const updatedLocalMediaProgress = [] | ||||||
|     var numServerProgressUpdates = 0 |     let numServerProgressUpdates = 0 | ||||||
|     const updatedServerMediaProgress = [] |     const updatedServerMediaProgress = [] | ||||||
|     const localMediaProgress = req.body.localMediaProgress || [] |     const localMediaProgress = req.body.localMediaProgress || [] | ||||||
| 
 | 
 | ||||||
|     localMediaProgress.forEach(localProgress => { |     for (const localProgress of localMediaProgress) { | ||||||
|       if (!localProgress.libraryItemId) { |       if (!localProgress.libraryItemId) { | ||||||
|         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress) |         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress) | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId) |       const libraryItem = Database.getLibraryItem(localProgress.libraryItemId) | ||||||
|       if (!libraryItem) { |       if (!libraryItem) { | ||||||
|         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress) |         Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress) | ||||||
|         return |         return | ||||||
| @ -199,12 +202,14 @@ class MeController { | |||||||
|         Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`) |         Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`) | ||||||
|         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) |         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) | ||||||
|         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) |         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) | ||||||
|  |         if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||||
|         updatedServerMediaProgress.push(mediaProgress) |         updatedServerMediaProgress.push(mediaProgress) | ||||||
|         numServerProgressUpdates++ |         numServerProgressUpdates++ | ||||||
|       } else if (mediaProgress.lastUpdate < localProgress.lastUpdate) { |       } else if (mediaProgress.lastUpdate < localProgress.lastUpdate) { | ||||||
|         Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`) |         Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`) | ||||||
|         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) |         req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) | ||||||
|         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) |         mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) | ||||||
|  |         if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) | ||||||
|         updatedServerMediaProgress.push(mediaProgress) |         updatedServerMediaProgress.push(mediaProgress) | ||||||
|         numServerProgressUpdates++ |         numServerProgressUpdates++ | ||||||
|       } else if (mediaProgress.lastUpdate > localProgress.lastUpdate) { |       } else if (mediaProgress.lastUpdate > localProgress.lastUpdate) { | ||||||
| @ -222,11 +227,10 @@ class MeController { | |||||||
|       } else { |       } else { | ||||||
|         Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`) |         Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`) | ||||||
|       } |       } | ||||||
|     }) |     } | ||||||
| 
 | 
 | ||||||
|     Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`) |     Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`) | ||||||
|     if (numServerProgressUpdates > 0) { |     if (numServerProgressUpdates > 0) { | ||||||
|       await this.db.updateEntity('user', req.user) |  | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -244,7 +248,7 @@ class MeController { | |||||||
|     let itemsInProgress = [] |     let itemsInProgress = [] | ||||||
|     for (const mediaProgress of req.user.mediaProgress) { |     for (const mediaProgress of req.user.mediaProgress) { | ||||||
|       if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) { |       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 (libraryItem) { | ||||||
|           if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') { |           if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') { | ||||||
|             const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId) |             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
 |   // GET: api/me/series/:id/remove-from-continue-listening
 | ||||||
|   async removeSeriesFromContinueListening(req, res) { |   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) { |     if (!series) { | ||||||
|       Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`) |       Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`) | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
| @ -282,7 +286,7 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|     const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id) |     const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('user', req.user) |       await Database.updateUser(req.user) | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
|     res.json(req.user.toJSONForBrowser()) |     res.json(req.user.toJSONForBrowser()) | ||||||
| @ -290,7 +294,7 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|   // GET: api/me/series/:id/readd-to-continue-listening
 |   // GET: api/me/series/:id/readd-to-continue-listening
 | ||||||
|   async readdSeriesFromContinueListening(req, res) { |   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) { |     if (!series) { | ||||||
|       Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`) |       Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`) | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
| @ -298,7 +302,7 @@ class MeController { | |||||||
| 
 | 
 | ||||||
|     const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id) |     const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('user', req.user) |       await Database.updateUser(req.user) | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
|     res.json(req.user.toJSONForBrowser()) |     res.json(req.user.toJSONForBrowser()) | ||||||
| @ -308,7 +312,7 @@ class MeController { | |||||||
|   async removeItemFromContinueListening(req, res) { |   async removeItemFromContinueListening(req, res) { | ||||||
|     const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id) |     const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('user', req.user) |       await Database.updateUser(req.user) | ||||||
|       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) |       SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||||
|     } |     } | ||||||
|     res.json(req.user.toJSONForBrowser()) |     res.json(req.user.toJSONForBrowser()) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ const Path = require('path') | |||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const filePerms = require('../utils/filePerms') | const filePerms = require('../utils/filePerms') | ||||||
| const patternValidation = require('../libs/nodeCron/pattern-validation') | const patternValidation = require('../libs/nodeCron/pattern-validation') | ||||||
| @ -30,7 +31,7 @@ class MiscController { | |||||||
|     var libraryId = req.body.library |     var libraryId = req.body.library | ||||||
|     var folderId = req.body.folder |     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) { |     if (!library) { | ||||||
|       return res.status(404).send(`Library not found with id ${libraryId}`) |       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') |       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 (madeUpdates) { | ||||||
|       // If backup schedule is updated - update backup manager
 |       // If backup schedule is updated - update backup manager
 | ||||||
|       if (settingsUpdate.backupSchedule !== undefined) { |       if (settingsUpdate.backupSchedule !== undefined) { | ||||||
|         this.backupManager.updateCronSchedule() |         this.backupManager.updateCronSchedule() | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       await this.db.updateServerSettings() |       await Database.updateServerSettings() | ||||||
|     } |     } | ||||||
|     return res.json({ |     return res.json({ | ||||||
|       success: true, |       success: true, | ||||||
|       serverSettings: this.db.serverSettings.toJSONForBrowser() |       serverSettings: Database.serverSettings.toJSONForBrowser() | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -147,7 +148,7 @@ class MiscController { | |||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
|     const tags = [] |     const tags = [] | ||||||
|     this.db.libraryItems.forEach((li) => { |     Database.libraryItems.forEach((li) => { | ||||||
|       if (li.media.tags && li.media.tags.length) { |       if (li.media.tags && li.media.tags.length) { | ||||||
|         li.media.tags.forEach((tag) => { |         li.media.tags.forEach((tag) => { | ||||||
|           if (!tags.includes(tag)) tags.push(tag) |           if (!tags.includes(tag)) tags.push(tag) | ||||||
| @ -176,7 +177,7 @@ class MiscController { | |||||||
|     let tagMerged = false |     let tagMerged = false | ||||||
|     let numItemsUpdated = 0 |     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 || !li.media.tags.length) continue | ||||||
| 
 | 
 | ||||||
|       if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
 |       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
 |           li.media.tags.push(newTag) // Add new tag
 | ||||||
|         } |         } | ||||||
|         Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`) |         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()) |         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||||
|         numItemsUpdated++ |         numItemsUpdated++ | ||||||
|       } |       } | ||||||
| @ -209,13 +210,13 @@ class MiscController { | |||||||
|     const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() |     const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() | ||||||
| 
 | 
 | ||||||
|     let numItemsUpdated = 0 |     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 || !li.media.tags.length) continue | ||||||
| 
 | 
 | ||||||
|       if (li.media.tags.includes(tag)) { |       if (li.media.tags.includes(tag)) { | ||||||
|         li.media.tags = li.media.tags.filter(t => t !== tag) |         li.media.tags = li.media.tags.filter(t => t !== tag) | ||||||
|         Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`) |         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()) |         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||||
|         numItemsUpdated++ |         numItemsUpdated++ | ||||||
|       } |       } | ||||||
| @ -233,7 +234,7 @@ class MiscController { | |||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
|     const genres = [] |     const genres = [] | ||||||
|     this.db.libraryItems.forEach((li) => { |     Database.libraryItems.forEach((li) => { | ||||||
|       if (li.media.metadata.genres && li.media.metadata.genres.length) { |       if (li.media.metadata.genres && li.media.metadata.genres.length) { | ||||||
|         li.media.metadata.genres.forEach((genre) => { |         li.media.metadata.genres.forEach((genre) => { | ||||||
|           if (!genres.includes(genre)) genres.push(genre) |           if (!genres.includes(genre)) genres.push(genre) | ||||||
| @ -262,7 +263,7 @@ class MiscController { | |||||||
|     let genreMerged = false |     let genreMerged = false | ||||||
|     let numItemsUpdated = 0 |     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 || !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
 |       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
 |           li.media.metadata.genres.push(newGenre) // Add new genre
 | ||||||
|         } |         } | ||||||
|         Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`) |         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()) |         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||||
|         numItemsUpdated++ |         numItemsUpdated++ | ||||||
|       } |       } | ||||||
| @ -295,13 +296,13 @@ class MiscController { | |||||||
|     const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() |     const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() | ||||||
| 
 | 
 | ||||||
|     let numItemsUpdated = 0 |     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 || !li.media.metadata.genres.length) continue | ||||||
| 
 | 
 | ||||||
|       if (li.media.metadata.genres.includes(genre)) { |       if (li.media.metadata.genres.includes(genre)) { | ||||||
|         li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== 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}"`) |         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()) |         SocketAuthority.emitter('item_updated', li.toJSONExpanded()) | ||||||
|         numItemsUpdated++ |         numItemsUpdated++ | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| const { version } = require('../../package.json') | const { version } = require('../../package.json') | ||||||
| 
 | 
 | ||||||
| class NotificationController { | class NotificationController { | ||||||
| @ -7,14 +8,14 @@ class NotificationController { | |||||||
|   get(req, res) { |   get(req, res) { | ||||||
|     res.json({ |     res.json({ | ||||||
|       data: this.notificationManager.getData(), |       data: this.notificationManager.getData(), | ||||||
|       settings: this.db.notificationSettings |       settings: Database.notificationSettings | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async update(req, res) { |   async update(req, res) { | ||||||
|     const updated = this.db.notificationSettings.update(req.body) |     const updated = Database.notificationSettings.update(req.body) | ||||||
|     if (updated) { |     if (updated) { | ||||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) |       await Database.updateSetting(Database.notificationSettings) | ||||||
|     } |     } | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| @ -29,31 +30,31 @@ class NotificationController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async createNotification(req, res) { |   async createNotification(req, res) { | ||||||
|     const success = this.db.notificationSettings.createNotification(req.body) |     const success = Database.notificationSettings.createNotification(req.body) | ||||||
| 
 | 
 | ||||||
|     if (success) { |     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) { |   async deleteNotification(req, res) { | ||||||
|     if (this.db.notificationSettings.removeNotification(req.notification.id)) { |     if (Database.notificationSettings.removeNotification(req.notification.id)) { | ||||||
|       await this.db.updateEntity('settings', this.db.notificationSettings) |       await Database.updateSetting(Database.notificationSettings) | ||||||
|     } |     } | ||||||
|     res.json(this.db.notificationSettings) |     res.json(Database.notificationSettings) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async updateNotification(req, res) { |   async updateNotification(req, res) { | ||||||
|     const success = this.db.notificationSettings.updateNotification(req.body) |     const success = Database.notificationSettings.updateNotification(req.body) | ||||||
|     if (success) { |     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) { |   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) |     const success = await this.notificationManager.sendTestNotification(req.notification) | ||||||
|     if (success) res.sendStatus(200) |     if (success) res.sendStatus(200) | ||||||
| @ -66,7 +67,7 @@ class NotificationController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (req.params.id) { |     if (req.params.id) { | ||||||
|       const notification = this.db.notificationSettings.getNotification(req.params.id) |       const notification = Database.notificationSettings.getNotification(req.params.id) | ||||||
|       if (!notification) { |       if (!notification) { | ||||||
|         return res.sendStatus(404) |         return res.sendStatus(404) | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const Playlist = require('../objects/Playlist') | const Playlist = require('../objects/Playlist') | ||||||
| 
 | 
 | ||||||
| @ -14,8 +15,8 @@ class PlaylistController { | |||||||
|     if (!success) { |     if (!success) { | ||||||
|       return res.status(400).send('Invalid playlist request data') |       return res.status(400).send('Invalid playlist request data') | ||||||
|     } |     } | ||||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems) | ||||||
|     await this.db.insertEntity('playlist', newPlaylist) |     await Database.createPlaylist(newPlaylist) | ||||||
|     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) |     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
|   } |   } | ||||||
| @ -23,22 +24,22 @@ class PlaylistController { | |||||||
|   // GET: api/playlists
 |   // GET: api/playlists
 | ||||||
|   findAllForUser(req, res) { |   findAllForUser(req, res) { | ||||||
|     res.json({ |     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
 |   // GET: api/playlists/:id
 | ||||||
|   findOne(req, res) { |   findOne(req, res) { | ||||||
|     res.json(req.playlist.toJSONExpanded(this.db.libraryItems)) |     res.json(req.playlist.toJSONExpanded(Database.libraryItems)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // PATCH: api/playlists/:id
 |   // PATCH: api/playlists/:id
 | ||||||
|   async update(req, res) { |   async update(req, res) { | ||||||
|     const playlist = req.playlist |     const playlist = req.playlist | ||||||
|     let wasUpdated = playlist.update(req.body) |     let wasUpdated = playlist.update(req.body) | ||||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||||
|     if (wasUpdated) { |     if (wasUpdated) { | ||||||
|       await this.db.updateEntity('playlist', playlist) |       await Database.updatePlaylist(playlist) | ||||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) |       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||||
|     } |     } | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
| @ -47,8 +48,8 @@ class PlaylistController { | |||||||
|   // DELETE: api/playlists/:id
 |   // DELETE: api/playlists/:id
 | ||||||
|   async delete(req, res) { |   async delete(req, res) { | ||||||
|     const playlist = req.playlist |     const playlist = req.playlist | ||||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||||
|     await this.db.removeEntity('playlist', playlist.id) |     await Database.removePlaylist(playlist.id) | ||||||
|     SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) |     SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| @ -62,7 +63,7 @@ class PlaylistController { | |||||||
|       return res.status(400).send('Request body has no libraryItemId') |       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) { |     if (!libraryItem) { | ||||||
|       return res.status(400).send('Library item not found') |       return res.status(400).send('Library item not found') | ||||||
|     } |     } | ||||||
| @ -80,8 +81,16 @@ class PlaylistController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId) |     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) |     SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
|   } |   } | ||||||
| @ -99,15 +108,15 @@ class PlaylistController { | |||||||
| 
 | 
 | ||||||
|     playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId) |     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
 |     // Playlist is removed when there are no items
 | ||||||
|     if (!playlist.items.length) { |     if (!playlist.items.length) { | ||||||
|       Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`) |       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) |       SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||||
|     } else { |     } else { | ||||||
|       await this.db.updateEntity('playlist', playlist) |       await Database.updatePlaylist(playlist) | ||||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) |       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -122,20 +131,34 @@ class PlaylistController { | |||||||
|     } |     } | ||||||
|     const itemsToAdd = req.body.items |     const itemsToAdd = req.body.items | ||||||
|     let hasUpdated = false |     let hasUpdated = false | ||||||
|  | 
 | ||||||
|  |     let order = playlist.items.length | ||||||
|  |     const playlistMediaItems = [] | ||||||
|     for (const item of itemsToAdd) { |     for (const item of itemsToAdd) { | ||||||
|       if (!item.libraryItemId) { |       if (!item.libraryItemId) { | ||||||
|         return res.status(400).send('Item does not have 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)) { |       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) |         playlist.addItem(item.libraryItemId, item.episodeId) | ||||||
|         hasUpdated = true |         hasUpdated = true | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('playlist', playlist) |       await Database.createBulkPlaylistMediaItems(playlistMediaItems) | ||||||
|       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) |       SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||||
|     } |     } | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
| @ -153,21 +176,22 @@ class PlaylistController { | |||||||
|       if (!item.libraryItemId) { |       if (!item.libraryItemId) { | ||||||
|         return res.status(400).send('Item does not have libraryItemId') |         return res.status(400).send('Item does not have libraryItemId') | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|       if (playlist.containsItem(item)) { |       if (playlist.containsItem(item)) { | ||||||
|         playlist.removeItem(item.libraryItemId, item.episodeId) |         playlist.removeItem(item.libraryItemId, item.episodeId) | ||||||
|         hasUpdated = true |         hasUpdated = true | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       // Playlist is removed when there are no items
 |       // Playlist is removed when there are no items
 | ||||||
|       if (!playlist.items.length) { |       if (!playlist.items.length) { | ||||||
|         Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`) |         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) |         SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded) | ||||||
|       } else { |       } else { | ||||||
|         await this.db.updateEntity('playlist', playlist) |         await Database.updatePlaylist(playlist) | ||||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) |         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -176,12 +200,12 @@ class PlaylistController { | |||||||
| 
 | 
 | ||||||
|   // POST: api/playlists/collection/:collectionId
 |   // POST: api/playlists/collection/:collectionId
 | ||||||
|   async createFromCollection(req, res) { |   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) { |     if (!collection) { | ||||||
|       return res.status(404).send('Collection not found') |       return res.status(404).send('Collection not found') | ||||||
|     } |     } | ||||||
|     // Expand collection to get library items
 |     // 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
 |     // Filter out library items not accessible to user
 | ||||||
|     const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item)) |     const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item)) | ||||||
| @ -201,15 +225,15 @@ class PlaylistController { | |||||||
|     } |     } | ||||||
|     newPlaylist.setData(newPlaylistData) |     newPlaylist.setData(newPlaylistData) | ||||||
| 
 | 
 | ||||||
|     const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems) |     const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems) | ||||||
|     await this.db.insertEntity('playlist', newPlaylist) |     await Database.createPlaylist(newPlaylist) | ||||||
|     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) |     SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded) | ||||||
|     res.json(jsonExpanded) |     res.json(jsonExpanded) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   middleware(req, res, next) { |   middleware(req, res, next) { | ||||||
|     if (req.params.id) { |     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) { |       if (!playlist) { | ||||||
|         return res.status(404).send('Playlist not found') |         return res.status(404).send('Playlist not found') | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| 
 | 
 | ||||||
| @ -18,7 +19,7 @@ class PodcastController { | |||||||
|     } |     } | ||||||
|     const payload = req.body |     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) { |     if (!library) { | ||||||
|       Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) |       Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`) | ||||||
|       return res.status(404).send('Library not found') |       return res.status(404).send('Library not found') | ||||||
| @ -33,7 +34,7 @@ class PodcastController { | |||||||
|     const podcastPath = filePathToPOSIX(payload.path) |     const podcastPath = filePathToPOSIX(payload.path) | ||||||
| 
 | 
 | ||||||
|     // Check if a library item with this podcast folder exists already
 |     // 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) { |     if (existingLibraryItem) { | ||||||
|       Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`) |       Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`) | ||||||
|       return res.status(400).send('Podcast already exists') |       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()) |     SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded()) | ||||||
| 
 | 
 | ||||||
|     res.json(libraryItem.toJSONExpanded()) |     res.json(libraryItem.toJSONExpanded()) | ||||||
| @ -199,7 +200,7 @@ class PodcastController { | |||||||
|     const overrideDetails = req.query.override === '1' |     const overrideDetails = req.query.override === '1' | ||||||
|     const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails }) |     const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails }) | ||||||
|     if (episodesUpdated) { |     if (episodesUpdated) { | ||||||
|       await this.db.updateLibraryItem(req.libraryItem) |       await Database.updateLibraryItem(req.libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -216,9 +217,8 @@ class PodcastController { | |||||||
|       return res.status(404).send('Episode not found') |       return res.status(404).send('Episode not found') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body) |     if (libraryItem.media.updateEpisode(episodeId, req.body)) { | ||||||
|     if (wasUpdated) { |       await Database.updateLibraryItem(libraryItem) | ||||||
|       await this.db.updateLibraryItem(libraryItem) |  | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -267,13 +267,13 @@ class PodcastController { | |||||||
|       libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino) |       libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     res.json(libraryItem.toJSON()) |     res.json(libraryItem.toJSON()) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   middleware(req, res, next) { |   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 || !item.media) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     if (!item.isPodcast) { |     if (!item.isPodcast) { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| class RSSFeedController { | class RSSFeedController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -8,7 +8,7 @@ class RSSFeedController { | |||||||
|   async openRSSFeedForItem(req, res) { |   async openRSSFeedForItem(req, res) { | ||||||
|     const options = req.body || {} |     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) |     if (!item) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     // Check user can access this library item
 |     // Check user can access this library item
 | ||||||
| @ -45,7 +45,7 @@ class RSSFeedController { | |||||||
|   async openRSSFeedForCollection(req, res) { |   async openRSSFeedForCollection(req, res) { | ||||||
|     const options = req.body || {} |     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) |     if (!collection) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     // Check request body options exist
 |     // Check request body options exist
 | ||||||
| @ -60,7 +60,7 @@ class RSSFeedController { | |||||||
|       return res.status(400).send('Slug already in use') |       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) |     const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length) | ||||||
| 
 | 
 | ||||||
|     // Check collection has audio tracks
 |     // Check collection has audio tracks
 | ||||||
| @ -79,7 +79,7 @@ class RSSFeedController { | |||||||
|   async openRSSFeedForSeries(req, res) { |   async openRSSFeedForSeries(req, res) { | ||||||
|     const options = req.body || {} |     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) |     if (!series) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|     // Check request body options exist
 |     // Check request body options exist
 | ||||||
| @ -96,7 +96,7 @@ class RSSFeedController { | |||||||
| 
 | 
 | ||||||
|     const seriesJson = series.toJSON() |     const seriesJson = series.toJSON() | ||||||
|     // Get books in series that have audio tracks
 |     // 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
 |     // Check series has audio tracks
 | ||||||
|     if (!seriesJson.books.length) { |     if (!seriesJson.books.length) { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| class SeriesController { | class SeriesController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -35,7 +36,7 @@ class SeriesController { | |||||||
|     var q = (req.query.q || '').toLowerCase() |     var q = (req.query.q || '').toLowerCase() | ||||||
|     if (!q) return res.json([]) |     if (!q) return res.json([]) | ||||||
|     var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25 |     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) |     series = series.slice(0, limit) | ||||||
|     res.json({ |     res.json({ | ||||||
|       results: series |       results: series | ||||||
| @ -45,17 +46,17 @@ class SeriesController { | |||||||
|   async update(req, res) { |   async update(req, res) { | ||||||
|     const hasUpdated = req.series.update(req.body) |     const hasUpdated = req.series.update(req.body) | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateEntity('series', req.series) |       await Database.updateSeries(req.series) | ||||||
|       SocketAuthority.emitter('series_updated', req.series.toJSON()) |       SocketAuthority.emitter('series_updated', req.series.toJSON()) | ||||||
|     } |     } | ||||||
|     res.json(req.series.toJSON()) |     res.json(req.series.toJSON()) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   middleware(req, res, next) { |   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) |     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))) { |     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) |       Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user) | ||||||
|       return res.sendStatus(403) |       return res.sendStatus(403) | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| const { toNumber } = require('../utils/index') | const { toNumber } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
| class SessionController { | class SessionController { | ||||||
| @ -49,7 +50,7 @@ class SessionController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const openSessions = this.playbackSessionManager.sessions.map(se => { |     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 { |       return { | ||||||
|         ...se.toJSON(), |         ...se.toJSON(), | ||||||
|         user: user ? { id: user.id, username: user.username } : null |         user: user ? { id: user.id, username: user.username } : null | ||||||
| @ -62,7 +63,7 @@ class SessionController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getOpenSession(req, res) { |   getOpenSession(req, res) { | ||||||
|     var libraryItem = this.db.getLibraryItem(req.session.libraryItemId) |     var libraryItem = Database.getLibraryItem(req.session.libraryItemId) | ||||||
|     var sessionForClient = req.session.toJSONForClient(libraryItem) |     var sessionForClient = req.session.toJSONForClient(libraryItem) | ||||||
|     res.json(sessionForClient) |     res.json(sessionForClient) | ||||||
|   } |   } | ||||||
| @ -87,7 +88,7 @@ class SessionController { | |||||||
|       await this.playbackSessionManager.removeSession(req.session.id) |       await this.playbackSessionManager.removeSession(req.session.id) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.db.removeEntity('session', req.session.id) |     await Database.removePlaybackSession(req.session.id) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -115,7 +116,7 @@ class SessionController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async middleware(req, res, next) { |   async middleware(req, res, next) { | ||||||
|     const playbackSession = await this.db.getPlaybackSession(req.params.id) |     const playbackSession = await Database.getPlaybackSession(req.params.id) | ||||||
|     if (!playbackSession) { |     if (!playbackSession) { | ||||||
|       Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`) |       Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`) | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| class ToolsController { | class ToolsController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -65,7 +66,7 @@ class ToolsController { | |||||||
| 
 | 
 | ||||||
|     const libraryItems = [] |     const libraryItems = [] | ||||||
|     for (const libraryItemId of libraryItemIds) { |     for (const libraryItemId of libraryItemIds) { | ||||||
|       const libraryItem = this.db.getLibraryItem(libraryItemId) |       const libraryItem = Database.getLibraryItem(libraryItemId) | ||||||
|       if (!libraryItem) { |       if (!libraryItem) { | ||||||
|         Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`) |         Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`) | ||||||
|         return res.sendStatus(404) |         return res.sendStatus(404) | ||||||
| @ -105,7 +106,7 @@ class ToolsController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (req.params.id) { |     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) |       if (!item || !item.media) return res.sendStatus(404) | ||||||
| 
 | 
 | ||||||
|       // Check user can access this library item
 |       // Check user can access this library item
 | ||||||
|  | |||||||
| @ -1,9 +1,11 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const User = require('../objects/user/User') | const User = require('../objects/user/User') | ||||||
| 
 | 
 | ||||||
| const { getId, toNumber } = require('../utils/index') | const { toNumber } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
| class UserController { | class UserController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -15,11 +17,11 @@ class UserController { | |||||||
|     const includes = (req.query.include || '').split(',').map(i => i.trim()) |     const includes = (req.query.include || '').split(',').map(i => i.trim()) | ||||||
| 
 | 
 | ||||||
|     // Minimal toJSONForBrowser does not include mediaProgress and bookmarks
 |     // 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')) { |     if (includes.includes('latestSession')) { | ||||||
|       for (const user of users) { |       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 |         user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -35,7 +37,7 @@ class UserController { | |||||||
|       return res.sendStatus(403) |       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) { |     if (!user) { | ||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
| @ -47,18 +49,19 @@ class UserController { | |||||||
|     var account = req.body |     var account = req.body | ||||||
| 
 | 
 | ||||||
|     var username = account.username |     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) { |     if (usernameExists) { | ||||||
|       return res.status(500).send('Username already taken') |       return res.status(500).send('Username already taken') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     account.id = getId('usr') |     account.id = uuidv4() | ||||||
|     account.pash = await this.auth.hashPass(account.password) |     account.pash = await this.auth.hashPass(account.password) | ||||||
|     delete account.password |     delete account.password | ||||||
|     account.token = await this.auth.generateAccessToken({ userId: account.id, username }) |     account.token = await this.auth.generateAccessToken({ userId: account.id, username }) | ||||||
|     account.createdAt = Date.now() |     account.createdAt = Date.now() | ||||||
|     var newUser = new User(account) |     const newUser = new User(account) | ||||||
|     var success = await this.db.insertEntity('user', newUser) | 
 | ||||||
|  |     const success = await Database.createUser(newUser) | ||||||
|     if (success) { |     if (success) { | ||||||
|       SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) |       SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) | ||||||
|       res.json({ |       res.json({ | ||||||
| @ -81,7 +84,7 @@ class UserController { | |||||||
|     var shouldUpdateToken = false |     var shouldUpdateToken = false | ||||||
| 
 | 
 | ||||||
|     if (account.username !== undefined && account.username !== user.username) { |     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) { |       if (usernameExists) { | ||||||
|         return res.status(500).send('Username already taken') |         return res.status(500).send('Username already taken') | ||||||
|       } |       } | ||||||
| @ -94,13 +97,12 @@ class UserController { | |||||||
|       delete account.password |       delete account.password | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var hasUpdated = user.update(account) |     if (user.update(account)) { | ||||||
|     if (hasUpdated) { |  | ||||||
|       if (shouldUpdateToken) { |       if (shouldUpdateToken) { | ||||||
|         user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) |         user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username }) | ||||||
|         Logger.info(`[UserController] User ${user.username} was generated a new api token`) |         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()) |       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
 |     // Todo: check if user is logged in and cancel streams
 | ||||||
| 
 | 
 | ||||||
|     // Remove user playlists
 |     // 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) { |     for (const playlist of userPlaylists) { | ||||||
|       await this.db.removeEntity('playlist', playlist.id) |       await Database.removePlaylist(playlist.id) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const userJson = user.toJSONForBrowser() |     const userJson = user.toJSONForBrowser() | ||||||
|     await this.db.removeEntity('user', user.id) |     await Database.removeUser(user.id) | ||||||
|     SocketAuthority.adminEmitter('user_removed', userJson) |     SocketAuthority.adminEmitter('user_removed', userJson) | ||||||
|     res.json({ |     res.json({ | ||||||
|       success: true |       success: true | ||||||
| @ -165,37 +167,39 @@ class UserController { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // POST: api/users/:id/purge-media-progress
 |   // POST: api/users/:id/purge-media-progress
 | ||||||
|  |   // TODO: Remove
 | ||||||
|   async purgeMediaProgress(req, res) { |   async purgeMediaProgress(req, res) { | ||||||
|     const user = req.reqUser |     return res.sendStatus(404) | ||||||
|  |     // const user = req.reqUser
 | ||||||
| 
 | 
 | ||||||
|     if (user.type === 'root' && !req.user.isRoot) { |     // if (user.type === 'root' && !req.user.isRoot) {
 | ||||||
|       Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username) |     //   Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
 | ||||||
|       return res.sendStatus(403) |     //   return res.sendStatus(403)
 | ||||||
|     } |     // }
 | ||||||
| 
 | 
 | ||||||
|     var progressPurged = 0 |     // var progressPurged = 0
 | ||||||
|     user.mediaProgress = user.mediaProgress.filter(mp => { |     // user.mediaProgress = user.mediaProgress.filter(mp => {
 | ||||||
|       const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) |     //   const libraryItem = Database.libraryItems.find(li => li.id === mp.libraryItemId)
 | ||||||
|       if (!libraryItem) { |     //   if (!libraryItem) {
 | ||||||
|         progressPurged++ |     //     progressPurged++
 | ||||||
|         return false |     //     return false
 | ||||||
|       } else if (mp.episodeId) { |     //   } else if (mp.episodeId) {
 | ||||||
|         const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null |     //     const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
 | ||||||
|         if (!episode) { // Episode not found
 |     //     if (!episode) { // Episode not found
 | ||||||
|           progressPurged++ |     //       progressPurged++
 | ||||||
|           return false |     //       return false
 | ||||||
|         } |     //     }
 | ||||||
|       } |     //   }
 | ||||||
|       return true |     //   return true
 | ||||||
|     }) |     // })
 | ||||||
| 
 | 
 | ||||||
|     if (progressPurged) { |     // if (progressPurged) {
 | ||||||
|       Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`) |     //   Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
 | ||||||
|       await this.db.updateEntity('user', user) |     //   await this.db.updateEntity('user', user)
 | ||||||
|       SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser()) |     //   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)
 |   // POST: api/users/online (admin)
 | ||||||
| @ -218,7 +222,7 @@ class UserController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (req.params.id) { |     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) { |       if (!req.reqUser) { | ||||||
|         return res.sendStatus(404) |         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') | const toneHelpers = require('../utils/toneHelpers') | ||||||
| 
 | 
 | ||||||
| class AbMergeManager { | class AbMergeManager { | ||||||
|   constructor(db, taskManager) { |   constructor(taskManager) { | ||||||
|     this.db = db |  | ||||||
|     this.taskManager = taskManager |     this.taskManager = taskManager | ||||||
| 
 | 
 | ||||||
|     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') |     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') | ||||||
|  | |||||||
| @ -10,8 +10,7 @@ const toneHelpers = require('../utils/toneHelpers') | |||||||
| const Task = require('../objects/Task') | const Task = require('../objects/Task') | ||||||
| 
 | 
 | ||||||
| class AudioMetadataMangaer { | class AudioMetadataMangaer { | ||||||
|   constructor(db, taskManager) { |   constructor(taskManager) { | ||||||
|     this.db = db |  | ||||||
|     this.taskManager = taskManager |     this.taskManager = taskManager | ||||||
| 
 | 
 | ||||||
|     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') |     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') | ||||||
|  | |||||||
| @ -10,15 +10,14 @@ const { downloadFile, filePathToPOSIX } = require('../utils/fileUtils') | |||||||
| const { extractCoverArt } = require('../utils/ffmpegHelpers') | const { extractCoverArt } = require('../utils/ffmpegHelpers') | ||||||
| 
 | 
 | ||||||
| class CoverManager { | class CoverManager { | ||||||
|   constructor(db, cacheManager) { |   constructor(cacheManager) { | ||||||
|     this.db = db |  | ||||||
|     this.cacheManager = cacheManager |     this.cacheManager = cacheManager | ||||||
| 
 | 
 | ||||||
|     this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items') |     this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items') | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getCoverDirectory(libraryItem) { |   getCoverDirectory(libraryItem) { | ||||||
|     if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { |     if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) { | ||||||
|       return libraryItem.path |       return libraryItem.path | ||||||
|     } else { |     } else { | ||||||
|       return Path.posix.join(this.ItemMetadataPath, libraryItem.id) |       return Path.posix.join(this.ItemMetadataPath, libraryItem.id) | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| const cron = require('../libs/nodeCron') | const cron = require('../libs/nodeCron') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| class CronManager { | class CronManager { | ||||||
|   constructor(db, scanner, podcastManager) { |   constructor(scanner, podcastManager) { | ||||||
|     this.db = db |  | ||||||
|     this.scanner = scanner |     this.scanner = scanner | ||||||
|     this.podcastManager = podcastManager |     this.podcastManager = podcastManager | ||||||
| 
 | 
 | ||||||
| @ -19,7 +19,7 @@ class CronManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   initLibraryScanCrons() { |   initLibraryScanCrons() { | ||||||
|     for (const library of this.db.libraries) { |     for (const library of Database.libraries) { | ||||||
|       if (library.settings.autoScanCronExpression) { |       if (library.settings.autoScanCronExpression) { | ||||||
|         this.startCronForLibrary(library) |         this.startCronForLibrary(library) | ||||||
|       } |       } | ||||||
| @ -64,7 +64,7 @@ class CronManager { | |||||||
| 
 | 
 | ||||||
|   initPodcastCrons() { |   initPodcastCrons() { | ||||||
|     const cronExpressionMap = {} |     const cronExpressionMap = {} | ||||||
|     this.db.libraryItems.forEach((li) => { |     Database.libraryItems.forEach((li) => { | ||||||
|       if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) { |       if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) { | ||||||
|         if (!li.media.autoDownloadSchedule) { |         if (!li.media.autoDownloadSchedule) { | ||||||
|           Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`) |           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
 |     // Get podcast library items to check
 | ||||||
|     const libraryItems = [] |     const libraryItems = [] | ||||||
|     for (const libraryItemId of libraryItemIds) { |     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) { |       if (!libraryItem) { | ||||||
|         Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`) |         Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`) | ||||||
|         podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
 |         podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
 | ||||||
|  | |||||||
| @ -1,14 +1,11 @@ | |||||||
| const nodemailer = require('nodemailer') | const nodemailer = require('nodemailer') | ||||||
| const Logger = require("../Logger") | const Logger = require("../Logger") | ||||||
| const SocketAuthority = require('../SocketAuthority') |  | ||||||
| 
 | 
 | ||||||
| class EmailManager { | class EmailManager { | ||||||
|   constructor(db) { |   constructor() { } | ||||||
|     this.db = db |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   getTransporter() { |   getTransporter() { | ||||||
|     return nodemailer.createTransport(this.db.emailSettings.getTransportObject()) |     return nodemailer.createTransport(Database.emailSettings.getTransportObject()) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async sendTest(res) { |   async sendTest(res) { | ||||||
| @ -25,8 +22,8 @@ class EmailManager { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     transporter.sendMail({ |     transporter.sendMail({ | ||||||
|       from: this.db.emailSettings.fromAddress, |       from: Database.emailSettings.fromAddress, | ||||||
|       to: this.db.emailSettings.testAddress || this.db.emailSettings.fromAddress, |       to: Database.emailSettings.testAddress || Database.emailSettings.fromAddress, | ||||||
|       subject: 'Test email from Audiobookshelf', |       subject: 'Test email from Audiobookshelf', | ||||||
|       text: 'Success!' |       text: 'Success!' | ||||||
|     }).then((result) => { |     }).then((result) => { | ||||||
| @ -52,7 +49,7 @@ class EmailManager { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     transporter.sendMail({ |     transporter.sendMail({ | ||||||
|       from: this.db.emailSettings.fromAddress, |       from: Database.emailSettings.fromAddress, | ||||||
|       to: device.email, |       to: device.email, | ||||||
|       subject: "Here is your Ebook!", |       subject: "Here is your Ebook!", | ||||||
|       html: '<div dir="auto"></div>', |       html: '<div dir="auto"></div>', | ||||||
|  | |||||||
| @ -9,9 +9,7 @@ const Logger = require('../Logger') | |||||||
| const TAG = '[LogManager]' | const TAG = '[LogManager]' | ||||||
| 
 | 
 | ||||||
| class LogManager { | class LogManager { | ||||||
|   constructor(db) { |   constructor() { | ||||||
|     this.db = db |  | ||||||
| 
 |  | ||||||
|     this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily') |     this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily') | ||||||
|     this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') |     this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') | ||||||
| 
 | 
 | ||||||
| @ -20,12 +18,8 @@ class LogManager { | |||||||
|     this.dailyLogFiles = [] |     this.dailyLogFiles = [] | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get serverSettings() { |  | ||||||
|     return this.db.serverSettings || {} |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   get loggerDailyLogsToKeep() { |   get loggerDailyLogsToKeep() { | ||||||
|     return this.serverSettings.loggerDailyLogsToKeep || 7 |     return global.ServerSettings.loggerDailyLogsToKeep || 7 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async ensureLogDirs() { |   async ensureLogDirs() { | ||||||
|  | |||||||
| @ -1,12 +1,11 @@ | |||||||
| const axios = require('axios') | const axios = require('axios') | ||||||
| const Logger = require("../Logger") | const Logger = require("../Logger") | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| const { notificationData } = require('../utils/notifications') | const { notificationData } = require('../utils/notifications') | ||||||
| 
 | 
 | ||||||
| class NotificationManager { | class NotificationManager { | ||||||
|   constructor(db) { |   constructor() { | ||||||
|     this.db = db |  | ||||||
| 
 |  | ||||||
|     this.sendingNotification = false |     this.sendingNotification = false | ||||||
|     this.notificationQueue = [] |     this.notificationQueue = [] | ||||||
|   } |   } | ||||||
| @ -16,10 +15,10 @@ class NotificationManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onPodcastEpisodeDownloaded(libraryItem, episode) { |   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}`) |     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 = { |     const eventData = { | ||||||
|       libraryItemId: libraryItem.id, |       libraryItemId: libraryItem.id, | ||||||
|       libraryId: libraryItem.libraryId, |       libraryId: libraryItem.libraryId, | ||||||
| @ -42,19 +41,19 @@ class NotificationManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async triggerNotification(eventName, eventData, intentionallyFail = false) { |   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
 |     // Will queue the notification if sendingNotification and queue is not full
 | ||||||
|     if (!this.checkTriggerNotification(eventName, eventData)) return |     if (!this.checkTriggerNotification(eventName, eventData)) return | ||||||
| 
 | 
 | ||||||
|     const notifications = this.db.notificationSettings.getActiveNotificationsForEvent(eventName) |     const notifications = Database.notificationSettings.getActiveNotificationsForEvent(eventName) | ||||||
|     for (const notification of notifications) { |     for (const notification of notifications) { | ||||||
|       Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`) |       Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`) | ||||||
|       const success = intentionallyFail ? false : await this.sendNotification(notification, eventData) |       const success = intentionallyFail ? false : await this.sendNotification(notification, eventData) | ||||||
| 
 | 
 | ||||||
|       notification.updateNotificationFired(success) |       notification.updateNotificationFired(success) | ||||||
|       if (!success) { // Failed notification
 |       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`) |           Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`) | ||||||
|           notification.enabled = false |           notification.enabled = false | ||||||
|         } else { |         } else { | ||||||
| @ -63,8 +62,8 @@ class NotificationManager { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.db.updateEntity('settings', this.db.notificationSettings) |     await Database.updateSetting(Database.notificationSettings) | ||||||
|     SocketAuthority.emitter('notifications_updated', this.db.notificationSettings.toJSON()) |     SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON()) | ||||||
| 
 | 
 | ||||||
|     this.notificationFinished() |     this.notificationFinished() | ||||||
|   } |   } | ||||||
| @ -72,7 +71,7 @@ class NotificationManager { | |||||||
|   // Return TRUE if notification should be triggered now
 |   // Return TRUE if notification should be triggered now
 | ||||||
|   checkTriggerNotification(eventName, eventData) { |   checkTriggerNotification(eventName, eventData) { | ||||||
|     if (this.sendingNotification) { |     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}`) |         Logger.warn(`[NotificationManager] Notification queue is full - ignoring event ${eventName}`) | ||||||
|       } else { |       } else { | ||||||
|         Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`) |         Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`) | ||||||
| @ -92,7 +91,7 @@ class NotificationManager { | |||||||
|         const nextNotificationEvent = this.notificationQueue.shift() |         const nextNotificationEvent = this.notificationQueue.shift() | ||||||
|         this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData) |         this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData) | ||||||
|       } |       } | ||||||
|     }, this.db.notificationSettings.notificationDelay) |     }, Database.notificationSettings.notificationDelay) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   sendTestNotification(notification) { |   sendTestNotification(notification) { | ||||||
| @ -107,7 +106,7 @@ class NotificationManager { | |||||||
| 
 | 
 | ||||||
|   sendNotification(notification, eventData) { |   sendNotification(notification, eventData) { | ||||||
|     const payload = notification.getApprisePayload(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) |       Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data) | ||||||
|       return true |       return true | ||||||
|     }).catch((error) => { |     }).catch((error) => { | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ const Path = require('path') | |||||||
| const serverVersion = require('../../package.json').version | const serverVersion = require('../../package.json').version | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const date = require('../libs/dateAndTime') | const date = require('../libs/dateAndTime') | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| @ -15,8 +16,7 @@ const DeviceInfo = require('../objects/DeviceInfo') | |||||||
| const Stream = require('../objects/Stream') | const Stream = require('../objects/Stream') | ||||||
| 
 | 
 | ||||||
| class PlaybackSessionManager { | class PlaybackSessionManager { | ||||||
|   constructor(db) { |   constructor() { | ||||||
|     this.db = db |  | ||||||
|     this.StreamsPath = Path.join(global.MetadataPath, 'streams') |     this.StreamsPath = Path.join(global.MetadataPath, 'streams') | ||||||
| 
 | 
 | ||||||
|     this.sessions = [] |     this.sessions = [] | ||||||
| @ -33,19 +33,31 @@ class PlaybackSessionManager { | |||||||
|     return session?.stream || null |     return session?.stream || null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getDeviceInfo(req) { |   async getDeviceInfo(req) { | ||||||
|     const ua = uaParserJs(req.headers['user-agent']) |     const ua = uaParserJs(req.headers['user-agent']) | ||||||
|     const ip = requestIp.getClientIp(req) |     const ip = requestIp.getClientIp(req) | ||||||
| 
 | 
 | ||||||
|     const clientDeviceInfo = req.body?.deviceInfo || null |     const clientDeviceInfo = req.body?.deviceInfo || null | ||||||
| 
 | 
 | ||||||
|     const deviceInfo = new DeviceInfo() |     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 |     return deviceInfo | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async startSessionRequest(req, res, episodeId) { |   async startSessionRequest(req, res, episodeId) { | ||||||
|     const deviceInfo = this.getDeviceInfo(req) |     const deviceInfo = await this.getDeviceInfo(req) | ||||||
|     Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) |     Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) | ||||||
|     const { user, libraryItem, body: options } = req |     const { user, libraryItem, body: options } = req | ||||||
|     const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) |     const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) | ||||||
| @ -77,7 +89,7 @@ class PlaybackSessionManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async syncLocalSession(user, sessionJson) { |   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 |     const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null | ||||||
|     if (!libraryItem || (libraryItem.isPodcast && !episode)) { |     if (!libraryItem || (libraryItem.isPodcast && !episode)) { | ||||||
|       Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`) |       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) { |     if (!session) { | ||||||
|       // New session from local
 |       // New session from local
 | ||||||
|       session = new PlaybackSession(sessionJson) |       session = new PlaybackSession(sessionJson) | ||||||
|       Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) |       Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) | ||||||
|       await this.db.insertEntity('session', session) |       await Database.createPlaybackSession(session) | ||||||
|     } else { |     } else { | ||||||
|       session.currentTime = sessionJson.currentTime |       session.currentTime = sessionJson.currentTime | ||||||
|       session.timeListening = sessionJson.timeListening |       session.timeListening = sessionJson.timeListening | ||||||
| @ -102,7 +114,7 @@ class PlaybackSessionManager { | |||||||
|       session.dayOfWeek = date.format(new Date(), 'dddd') |       session.dayOfWeek = date.format(new Date(), 'dddd') | ||||||
| 
 | 
 | ||||||
|       Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`) |       Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`) | ||||||
|       await this.db.updateEntity('session', session) |       await Database.updatePlaybackSession(session) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const result = { |     const result = { | ||||||
| @ -126,8 +138,8 @@ class PlaybackSessionManager { | |||||||
| 
 | 
 | ||||||
|     // Update user and emit socket event
 |     // Update user and emit socket event
 | ||||||
|     if (result.progressSynced) { |     if (result.progressSynced) { | ||||||
|       await this.db.updateEntity('user', user) |  | ||||||
|       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) |       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) | ||||||
|  |       if (itemProgress) await Database.upsertMediaProgress(itemProgress) | ||||||
|       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { |       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { | ||||||
|         id: itemProgress.id, |         id: itemProgress.id, | ||||||
|         sessionId: session.id, |         sessionId: session.id, | ||||||
| @ -155,7 +167,7 @@ class PlaybackSessionManager { | |||||||
| 
 | 
 | ||||||
|   async startSession(user, deviceInfo, libraryItem, episodeId, options) { |   async startSession(user, deviceInfo, libraryItem, episodeId, options) { | ||||||
|     // Close any sessions already open for user and device
 |     // 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) { |     for (const session of userSessions) { | ||||||
|       Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`) |       Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`) | ||||||
|       await this.closeSession(user, session, null) |       await this.closeSession(user, session, null) | ||||||
| @ -213,13 +225,13 @@ class PlaybackSessionManager { | |||||||
|     user.currentSessionId = newPlaybackSession.id |     user.currentSessionId = newPlaybackSession.id | ||||||
| 
 | 
 | ||||||
|     this.sessions.push(newPlaybackSession) |     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 |     return newPlaybackSession | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async syncSession(user, session, syncData) { |   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) { |     if (!libraryItem) { | ||||||
|       Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) |       Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) | ||||||
|       return null |       return null | ||||||
| @ -236,9 +248,8 @@ class PlaybackSessionManager { | |||||||
|     } |     } | ||||||
|     const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) |     const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) | ||||||
|     if (wasUpdated) { |     if (wasUpdated) { | ||||||
| 
 |  | ||||||
|       await this.db.updateEntity('user', user) |  | ||||||
|       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) |       const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) | ||||||
|  |       if (itemProgress) await Database.upsertMediaProgress(itemProgress) | ||||||
|       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { |       SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { | ||||||
|         id: itemProgress.id, |         id: itemProgress.id, | ||||||
|         sessionId: session.id, |         sessionId: session.id, | ||||||
| @ -259,7 +270,7 @@ class PlaybackSessionManager { | |||||||
|       await this.saveSession(session) |       await this.saveSession(session) | ||||||
|     } |     } | ||||||
|     Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`) |     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) |     SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id) | ||||||
|     return this.removeSession(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.timeListening) return // Do not save a session with no listening time
 | ||||||
| 
 | 
 | ||||||
|     if (session.lastSave) { |     if (session.lastSave) { | ||||||
|       return this.db.updateEntity('session', session) |       return Database.updatePlaybackSession(session) | ||||||
|     } else { |     } else { | ||||||
|       session.lastSave = Date.now() |       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) |       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 | module.exports = PlaybackSessionManager | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| 
 | 
 | ||||||
| @ -19,8 +20,7 @@ const AudioFile = require('../objects/files/AudioFile') | |||||||
| const Task = require("../objects/Task") | const Task = require("../objects/Task") | ||||||
| 
 | 
 | ||||||
| class PodcastManager { | class PodcastManager { | ||||||
|   constructor(db, watcher, notificationManager, taskManager) { |   constructor(watcher, notificationManager, taskManager) { | ||||||
|     this.db = db |  | ||||||
|     this.watcher = watcher |     this.watcher = watcher | ||||||
|     this.notificationManager = notificationManager |     this.notificationManager = notificationManager | ||||||
|     this.taskManager = taskManager |     this.taskManager = taskManager | ||||||
| @ -32,10 +32,6 @@ class PodcastManager { | |||||||
|     this.MaxFailedEpisodeChecks = 24 |     this.MaxFailedEpisodeChecks = 24 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get serverSettings() { |  | ||||||
|     return this.db.serverSettings || {} |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getEpisodeDownloadsInQueue(libraryItemId) { |   getEpisodeDownloadsInQueue(libraryItemId) { | ||||||
|     return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId) |     return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId) | ||||||
|   } |   } | ||||||
| @ -59,6 +55,7 @@ class PodcastManager { | |||||||
|       const newPe = new PodcastEpisode() |       const newPe = new PodcastEpisode() | ||||||
|       newPe.setData(ep, index++) |       newPe.setData(ep, index++) | ||||||
|       newPe.libraryItemId = libraryItem.id |       newPe.libraryItemId = libraryItem.id | ||||||
|  |       newPe.podcastId = libraryItem.media.id | ||||||
|       const newPeDl = new PodcastEpisodeDownload() |       const newPeDl = new PodcastEpisodeDownload() | ||||||
|       newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId) |       newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId) | ||||||
|       this.startPodcastEpisodeDownload(newPeDl) |       this.startPodcastEpisodeDownload(newPeDl) | ||||||
| @ -153,7 +150,7 @@ class PodcastManager { | |||||||
|       return false |       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) { |     if (!libraryItem) { | ||||||
|       Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`) |       Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`) | ||||||
|       return false |       return false | ||||||
| @ -182,7 +179,7 @@ class PodcastManager { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     libraryItem.updatedAt = Date.now() |     libraryItem.updatedAt = Date.now() | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded() |     const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded() | ||||||
|     podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded() |     podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded() | ||||||
| @ -235,6 +232,7 @@ class PodcastManager { | |||||||
|     } |     } | ||||||
|     const newAudioFile = new AudioFile() |     const newAudioFile = new AudioFile() | ||||||
|     newAudioFile.setDataFromProbe(libraryFile, mediaProbeData) |     newAudioFile.setDataFromProbe(libraryFile, mediaProbeData) | ||||||
|  |     newAudioFile.index = 1 | ||||||
|     return newAudioFile |     return newAudioFile | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -274,7 +272,7 @@ class PodcastManager { | |||||||
| 
 | 
 | ||||||
|     libraryItem.media.lastEpisodeCheck = Date.now() |     libraryItem.media.lastEpisodeCheck = Date.now() | ||||||
|     libraryItem.updatedAt = Date.now() |     libraryItem.updatedAt = Date.now() | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     return libraryItem.media.autoDownloadEpisodes |     return libraryItem.media.autoDownloadEpisodes | ||||||
|   } |   } | ||||||
| @ -313,7 +311,7 @@ class PodcastManager { | |||||||
| 
 | 
 | ||||||
|     libraryItem.media.lastEpisodeCheck = Date.now() |     libraryItem.media.lastEpisodeCheck = Date.now() | ||||||
|     libraryItem.updatedAt = Date.now() |     libraryItem.updatedAt = Date.now() | ||||||
|     await this.db.updateLibraryItem(libraryItem) |     await Database.updateLibraryItem(libraryItem) | ||||||
|     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
| 
 | 
 | ||||||
|     return newEpisodes |     return newEpisodes | ||||||
|  | |||||||
| @ -2,14 +2,13 @@ const Path = require('path') | |||||||
| 
 | 
 | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| const Feed = require('../objects/Feed') | const Feed = require('../objects/Feed') | ||||||
| 
 | 
 | ||||||
| class RssFeedManager { | class RssFeedManager { | ||||||
|   constructor(db) { |   constructor() { | ||||||
|     this.db = db |  | ||||||
| 
 |  | ||||||
|     this.feeds = {} |     this.feeds = {} | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -19,18 +18,18 @@ class RssFeedManager { | |||||||
| 
 | 
 | ||||||
|   validateFeedEntity(feedObj) { |   validateFeedEntity(feedObj) { | ||||||
|     if (feedObj.entityType === 'collection') { |     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`) |         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`) | ||||||
|         return false |         return false | ||||||
|       } |       } | ||||||
|     } else if (feedObj.entityType === 'libraryItem') { |     } 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`) |         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`) | ||||||
|         return false |         return false | ||||||
|       } |       } | ||||||
|     } else if (feedObj.entityType === 'series') { |     } else if (feedObj.entityType === 'series') { | ||||||
|       const series = this.db.series.find(s => s.id === feedObj.entityId) |       const series = Database.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 hasSeriesBook = Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) | ||||||
|       if (!hasSeriesBook) { |       if (!hasSeriesBook) { | ||||||
|         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`) |         Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`) | ||||||
|         return false |         return false | ||||||
| @ -43,19 +42,13 @@ class RssFeedManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async init() { |   async init() { | ||||||
|     const feedObjects = await this.db.getAllEntities('feed') |     const feedObjects = Database.feeds | ||||||
|     if (!feedObjects || !feedObjects.length) return |     if (!feedObjects?.length) return | ||||||
| 
 | 
 | ||||||
|     for (const feedObj of feedObjects) { |     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
 |       // Remove invalid feeds
 | ||||||
|       if (!this.validateFeedEntity(feedObj)) { |       if (!this.validateFeedEntity(feedObj)) { | ||||||
|         await this.db.removeEntity('feed', feedObj.id) |         await Database.removeFeed(feedObj.id) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const feed = new Feed(feedObj) |       const feed = new Feed(feedObj) | ||||||
| @ -82,7 +75,7 @@ class RssFeedManager { | |||||||
| 
 | 
 | ||||||
|     // Check if feed needs to be updated
 |     // Check if feed needs to be updated
 | ||||||
|     if (feed.entityType === 'libraryItem') { |     if (feed.entityType === 'libraryItem') { | ||||||
|       const libraryItem = this.db.getLibraryItem(feed.entityId) |       const libraryItem = Database.getLibraryItem(feed.entityId) | ||||||
| 
 | 
 | ||||||
|       let mostRecentlyUpdatedAt = libraryItem.updatedAt |       let mostRecentlyUpdatedAt = libraryItem.updatedAt | ||||||
|       if (libraryItem.isPodcast) { |       if (libraryItem.isPodcast) { | ||||||
| @ -94,12 +87,12 @@ class RssFeedManager { | |||||||
|       if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) { |       if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) { | ||||||
|         Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`) |         Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`) | ||||||
|         feed.updateFromItem(libraryItem) |         feed.updateFromItem(libraryItem) | ||||||
|         await this.db.updateEntity('feed', feed) |         await Database.updateFeed(feed) | ||||||
|       } |       } | ||||||
|     } else if (feed.entityType === 'collection') { |     } 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) { |       if (collection) { | ||||||
|         const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems) |         const collectionExpanded = collection.toJSONExpanded(Database.libraryItems) | ||||||
| 
 | 
 | ||||||
|         // Find most recently updated item in collection
 |         // Find most recently updated item in collection
 | ||||||
|         let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate |         let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate | ||||||
| @ -113,15 +106,15 @@ class RssFeedManager { | |||||||
|           Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) |           Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) | ||||||
| 
 | 
 | ||||||
|           feed.updateFromCollection(collectionExpanded) |           feed.updateFromCollection(collectionExpanded) | ||||||
|           await this.db.updateEntity('feed', feed) |           await Database.updateFeed(feed) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } else if (feed.entityType === 'series') { |     } 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) { |       if (series) { | ||||||
|         const seriesJson = series.toJSON() |         const seriesJson = series.toJSON() | ||||||
|         // Get books in series that have audio tracks
 |         // 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
 |         // Find most recently updated item in series
 | ||||||
|         let mostRecentlyUpdatedAt = seriesJson.updatedAt |         let mostRecentlyUpdatedAt = seriesJson.updatedAt | ||||||
| @ -140,7 +133,7 @@ class RssFeedManager { | |||||||
|           Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`) |           Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`) | ||||||
| 
 | 
 | ||||||
|           feed.updateFromSeries(seriesJson) |           feed.updateFromSeries(seriesJson) | ||||||
|           await this.db.updateEntity('feed', feed) |           await Database.updateFeed(feed) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -197,7 +190,7 @@ class RssFeedManager { | |||||||
|     this.feeds[feed.id] = feed |     this.feeds[feed.id] = feed | ||||||
| 
 | 
 | ||||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) |     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()) |     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||||
|     return feed |     return feed | ||||||
|   } |   } | ||||||
| @ -214,7 +207,7 @@ class RssFeedManager { | |||||||
|     this.feeds[feed.id] = feed |     this.feeds[feed.id] = feed | ||||||
| 
 | 
 | ||||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) |     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()) |     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||||
|     return feed |     return feed | ||||||
|   } |   } | ||||||
| @ -231,14 +224,14 @@ class RssFeedManager { | |||||||
|     this.feeds[feed.id] = feed |     this.feeds[feed.id] = feed | ||||||
| 
 | 
 | ||||||
|     Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) |     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()) |     SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) | ||||||
|     return feed |     return feed | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async handleCloseFeed(feed) { |   async handleCloseFeed(feed) { | ||||||
|     if (!feed) return |     if (!feed) return | ||||||
|     await this.db.removeEntity('feed', feed.id) |     await Database.removeFeed(feed.id) | ||||||
|     SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified()) |     SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified()) | ||||||
|     delete this.feeds[feed.id] |     delete this.feeds[feed.id] | ||||||
|     Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`) |     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 { | class Collection { | ||||||
|   constructor(collection) { |   constructor(collection) { | ||||||
|     this.id = null |     this.id = null | ||||||
|     this.libraryId = null |     this.libraryId = null | ||||||
|     this.userId = null |  | ||||||
| 
 | 
 | ||||||
|     this.name = null |     this.name = null | ||||||
|     this.description = null |     this.description = null | ||||||
| @ -25,7 +24,6 @@ class Collection { | |||||||
|     return { |     return { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|       libraryId: this.libraryId, |       libraryId: this.libraryId, | ||||||
|       userId: this.userId, |  | ||||||
|       name: this.name, |       name: this.name, | ||||||
|       description: this.description, |       description: this.description, | ||||||
|       cover: this.cover, |       cover: this.cover, | ||||||
| @ -60,7 +58,6 @@ class Collection { | |||||||
|   construct(collection) { |   construct(collection) { | ||||||
|     this.id = collection.id |     this.id = collection.id | ||||||
|     this.libraryId = collection.libraryId |     this.libraryId = collection.libraryId | ||||||
|     this.userId = collection.userId |  | ||||||
|     this.name = collection.name |     this.name = collection.name | ||||||
|     this.description = collection.description || null |     this.description = collection.description || null | ||||||
|     this.cover = collection.cover || null |     this.cover = collection.cover || null | ||||||
| @ -71,11 +68,10 @@ class Collection { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data) { |   setData(data) { | ||||||
|     if (!data.userId || !data.libraryId || !data.name) { |     if (!data.libraryId || !data.name) { | ||||||
|       return false |       return false | ||||||
|     } |     } | ||||||
|     this.id = getId('col') |     this.id = uuidv4() | ||||||
|     this.userId = data.userId |  | ||||||
|     this.libraryId = data.libraryId |     this.libraryId = data.libraryId | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|     this.description = data.description || null |     this.description = data.description || null | ||||||
|  | |||||||
| @ -1,5 +1,9 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
|  | 
 | ||||||
| class DeviceInfo { | class DeviceInfo { | ||||||
|   constructor(deviceInfo = null) { |   constructor(deviceInfo = null) { | ||||||
|  |     this.id = null | ||||||
|  |     this.userId = null | ||||||
|     this.deviceId = null |     this.deviceId = null | ||||||
|     this.ipAddress = null |     this.ipAddress = null | ||||||
| 
 | 
 | ||||||
| @ -16,7 +20,8 @@ class DeviceInfo { | |||||||
|     this.model = null |     this.model = null | ||||||
|     this.sdkVersion = null // Android Only
 |     this.sdkVersion = null // Android Only
 | ||||||
| 
 | 
 | ||||||
|     this.serverVersion = null |     this.clientName = null | ||||||
|  |     this.deviceName = null | ||||||
| 
 | 
 | ||||||
|     if (deviceInfo) { |     if (deviceInfo) { | ||||||
|       this.construct(deviceInfo) |       this.construct(deviceInfo) | ||||||
| @ -33,6 +38,8 @@ class DeviceInfo { | |||||||
| 
 | 
 | ||||||
|   toJSON() { |   toJSON() { | ||||||
|     const obj = { |     const obj = { | ||||||
|  |       id: this.id, | ||||||
|  |       userId: this.userId, | ||||||
|       deviceId: this.deviceId, |       deviceId: this.deviceId, | ||||||
|       ipAddress: this.ipAddress, |       ipAddress: this.ipAddress, | ||||||
|       browserName: this.browserName, |       browserName: this.browserName, | ||||||
| @ -44,7 +51,8 @@ class DeviceInfo { | |||||||
|       manufacturer: this.manufacturer, |       manufacturer: this.manufacturer, | ||||||
|       model: this.model, |       model: this.model, | ||||||
|       sdkVersion: this.sdkVersion, |       sdkVersion: this.sdkVersion, | ||||||
|       serverVersion: this.serverVersion |       clientName: this.clientName, | ||||||
|  |       deviceName: this.deviceName | ||||||
|     } |     } | ||||||
|     for (const key in obj) { |     for (const key in obj) { | ||||||
|       if (obj[key] === null || obj[key] === undefined) { |       if (obj[key] === null || obj[key] === undefined) { | ||||||
| @ -65,6 +73,7 @@ class DeviceInfo { | |||||||
|   // When client doesn't send a device id
 |   // When client doesn't send a device id
 | ||||||
|   getTempDeviceId() { |   getTempDeviceId() { | ||||||
|     const keys = [ |     const keys = [ | ||||||
|  |       this.userId, | ||||||
|       this.browserName, |       this.browserName, | ||||||
|       this.browserVersion, |       this.browserVersion, | ||||||
|       this.osName, |       this.osName, | ||||||
| @ -78,7 +87,9 @@ class DeviceInfo { | |||||||
|     return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') |     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.deviceId = clientDeviceInfo?.deviceId || null | ||||||
|     this.ipAddress = ip || null |     this.ipAddress = ip || null | ||||||
| 
 | 
 | ||||||
| @ -88,16 +99,54 @@ class DeviceInfo { | |||||||
|     this.osVersion = ua?.os.version || null |     this.osVersion = ua?.os.version || null | ||||||
|     this.deviceType = ua?.device.type || null |     this.deviceType = ua?.device.type || null | ||||||
| 
 | 
 | ||||||
|     this.clientVersion = clientDeviceInfo?.clientVersion || null |     this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion | ||||||
|     this.manufacturer = clientDeviceInfo?.manufacturer || null |     this.manufacturer = clientDeviceInfo?.manufacturer || null | ||||||
|     this.model = clientDeviceInfo?.model || null |     this.model = clientDeviceInfo?.model || null | ||||||
|     this.sdkVersion = clientDeviceInfo?.sdkVersion || 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) { |     if (!this.deviceId) { | ||||||
|       this.deviceId = this.getTempDeviceId() |       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 | module.exports = DeviceInfo | ||||||
| @ -1,3 +1,4 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const FeedMeta = require('./FeedMeta') | const FeedMeta = require('./FeedMeta') | ||||||
| const FeedEpisode = require('./FeedEpisode') | const FeedEpisode = require('./FeedEpisode') | ||||||
| const RSS = require('../libs/rss') | const RSS = require('../libs/rss') | ||||||
| @ -90,7 +91,7 @@ class Feed { | |||||||
|     const feedUrl = `${serverAddress}/feed/${slug}` |     const feedUrl = `${serverAddress}/feed/${slug}` | ||||||
|     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName |     const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName | ||||||
| 
 | 
 | ||||||
|     this.id = slug |     this.id = uuidv4() | ||||||
|     this.slug = slug |     this.slug = slug | ||||||
|     this.userId = userId |     this.userId = userId | ||||||
|     this.entityType = 'libraryItem' |     this.entityType = 'libraryItem' | ||||||
| @ -179,7 +180,7 @@ class Feed { | |||||||
|     const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) |     const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length) | ||||||
|     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) |     const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath) | ||||||
| 
 | 
 | ||||||
|     this.id = slug |     this.id = uuidv4() | ||||||
|     this.slug = slug |     this.slug = slug | ||||||
|     this.userId = userId |     this.userId = userId | ||||||
|     this.entityType = 'collection' |     this.entityType = 'collection' | ||||||
| @ -253,7 +254,7 @@ class Feed { | |||||||
|     const libraryId = itemsWithTracks[0].libraryId |     const libraryId = itemsWithTracks[0].libraryId | ||||||
|     const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath) |     const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath) | ||||||
| 
 | 
 | ||||||
|     this.id = slug |     this.id = uuidv4() | ||||||
|     this.slug = slug |     this.slug = slug | ||||||
|     this.userId = userId |     this.userId = userId | ||||||
|     this.entityType = 'series' |     this.entityType = 'series' | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| const Path = require('path') |  | ||||||
| const date = require('../libs/dateAndTime') | const date = require('../libs/dateAndTime') | ||||||
| const { secondsToTimestamp } = require('../utils/index') | const { secondsToTimestamp } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const { getId } = require("../utils") | const uuidv4 = require("uuid").v4 | ||||||
| 
 | 
 | ||||||
| class Folder { | class Folder { | ||||||
|   constructor(folder = null) { |   constructor(folder = null) { | ||||||
| @ -29,7 +29,7 @@ class Folder { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data) { |   setData(data) { | ||||||
|     this.id = data.id ? data.id : getId('fol') |     this.id = data.id || uuidv4() | ||||||
|     this.fullPath = data.fullPath |     this.fullPath = data.fullPath | ||||||
|     this.libraryId = data.libraryId |     this.libraryId = data.libraryId | ||||||
|     this.addedAt = Date.now() |     this.addedAt = Date.now() | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const Folder = require('./Folder') | const Folder = require('./Folder') | ||||||
| const LibrarySettings = require('./settings/LibrarySettings') | const LibrarySettings = require('./settings/LibrarySettings') | ||||||
| const { getId } = require('../utils/index') |  | ||||||
| const { filePathToPOSIX } = require('../utils/fileUtils') | const { filePathToPOSIX } = require('../utils/fileUtils') | ||||||
| 
 | 
 | ||||||
| class Library { | class Library { | ||||||
| @ -87,7 +87,7 @@ class Library { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data) { |   setData(data) { | ||||||
|     this.id = data.id ? data.id : getId('lib') |     this.id = data.id || uuidv4() | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|     if (data.folder) { |     if (data.folder) { | ||||||
|       this.folders = [ |       this.folders = [ | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const { version } = require('../../package.json') | const { version } = require('../../package.json') | ||||||
| @ -8,7 +9,7 @@ const Book = require('./mediaTypes/Book') | |||||||
| const Podcast = require('./mediaTypes/Podcast') | const Podcast = require('./mediaTypes/Podcast') | ||||||
| const Video = require('./mediaTypes/Video') | const Video = require('./mediaTypes/Video') | ||||||
| const Music = require('./mediaTypes/Music') | 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') | const { filePathToPOSIX } = require('../utils/fileUtils') | ||||||
| 
 | 
 | ||||||
| class LibraryItem { | class LibraryItem { | ||||||
| @ -191,7 +192,7 @@ class LibraryItem { | |||||||
| 
 | 
 | ||||||
|   // Data comes from scandir library item data
 |   // Data comes from scandir library item data
 | ||||||
|   setData(libraryMediaType, payload) { |   setData(libraryMediaType, payload) { | ||||||
|     this.id = getId('li') |     this.id = uuidv4() | ||||||
|     this.mediaType = libraryMediaType |     this.mediaType = libraryMediaType | ||||||
|     if (libraryMediaType === 'video') { |     if (libraryMediaType === 'video') { | ||||||
|       this.media = new Video() |       this.media = new Video() | ||||||
| @ -202,6 +203,7 @@ class LibraryItem { | |||||||
|     } else if (libraryMediaType === 'music') { |     } else if (libraryMediaType === 'music') { | ||||||
|       this.media = new Music() |       this.media = new Music() | ||||||
|     } |     } | ||||||
|  |     this.media.id = uuidv4() | ||||||
|     this.media.libraryItemId = this.id |     this.media.libraryItemId = this.id | ||||||
| 
 | 
 | ||||||
|     for (const key in payload) { |     for (const key in payload) { | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const { getId } = require('../utils/index') | const uuidv4 = require("uuid").v4 | ||||||
| 
 | 
 | ||||||
| class Notification { | class Notification { | ||||||
|   constructor(notification = null) { |   constructor(notification = null) { | ||||||
| @ -57,7 +57,7 @@ class Notification { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(payload) { |   setData(payload) { | ||||||
|     this.id = getId('noti') |     this.id = uuidv4() | ||||||
|     this.libraryId = payload.libraryId || null |     this.libraryId = payload.libraryId || null | ||||||
|     this.eventName = payload.eventName |     this.eventName = payload.eventName | ||||||
|     this.urls = payload.urls |     this.urls = payload.urls | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const date = require('../libs/dateAndTime') | 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 BookMetadata = require('./metadata/BookMetadata') | ||||||
| const PodcastMetadata = require('./metadata/PodcastMetadata') | const PodcastMetadata = require('./metadata/PodcastMetadata') | ||||||
| const DeviceInfo = require('./DeviceInfo') | const DeviceInfo = require('./DeviceInfo') | ||||||
| @ -11,6 +12,7 @@ class PlaybackSession { | |||||||
|     this.userId = null |     this.userId = null | ||||||
|     this.libraryId = null |     this.libraryId = null | ||||||
|     this.libraryItemId = null |     this.libraryItemId = null | ||||||
|  |     this.bookId = null | ||||||
|     this.episodeId = null |     this.episodeId = null | ||||||
| 
 | 
 | ||||||
|     this.mediaType = null |     this.mediaType = null | ||||||
| @ -24,6 +26,7 @@ class PlaybackSession { | |||||||
|     this.playMethod = null |     this.playMethod = null | ||||||
|     this.mediaPlayer = null |     this.mediaPlayer = null | ||||||
|     this.deviceInfo = null |     this.deviceInfo = null | ||||||
|  |     this.serverVersion = null | ||||||
| 
 | 
 | ||||||
|     this.date = null |     this.date = null | ||||||
|     this.dayOfWeek = null |     this.dayOfWeek = null | ||||||
| @ -52,6 +55,7 @@ class PlaybackSession { | |||||||
|       userId: this.userId, |       userId: this.userId, | ||||||
|       libraryId: this.libraryId, |       libraryId: this.libraryId, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|  |       bookId: this.bookId, | ||||||
|       episodeId: this.episodeId, |       episodeId: this.episodeId, | ||||||
|       mediaType: this.mediaType, |       mediaType: this.mediaType, | ||||||
|       mediaMetadata: this.mediaMetadata?.toJSON() || null, |       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||||
| @ -63,6 +67,7 @@ class PlaybackSession { | |||||||
|       playMethod: this.playMethod, |       playMethod: this.playMethod, | ||||||
|       mediaPlayer: this.mediaPlayer, |       mediaPlayer: this.mediaPlayer, | ||||||
|       deviceInfo: this.deviceInfo?.toJSON() || null, |       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||||
|  |       serverVersion: this.serverVersion, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
| @ -79,6 +84,7 @@ class PlaybackSession { | |||||||
|       userId: this.userId, |       userId: this.userId, | ||||||
|       libraryId: this.libraryId, |       libraryId: this.libraryId, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|  |       bookId: this.bookId, | ||||||
|       episodeId: this.episodeId, |       episodeId: this.episodeId, | ||||||
|       mediaType: this.mediaType, |       mediaType: this.mediaType, | ||||||
|       mediaMetadata: this.mediaMetadata?.toJSON() || null, |       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||||
| @ -90,6 +96,7 @@ class PlaybackSession { | |||||||
|       playMethod: this.playMethod, |       playMethod: this.playMethod, | ||||||
|       mediaPlayer: this.mediaPlayer, |       mediaPlayer: this.mediaPlayer, | ||||||
|       deviceInfo: this.deviceInfo?.toJSON() || null, |       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||||
|  |       serverVersion: this.serverVersion, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
| @ -108,12 +115,20 @@ class PlaybackSession { | |||||||
|     this.userId = session.userId |     this.userId = session.userId | ||||||
|     this.libraryId = session.libraryId || null |     this.libraryId = session.libraryId || null | ||||||
|     this.libraryItemId = session.libraryItemId |     this.libraryItemId = session.libraryItemId | ||||||
|  |     this.bookId = session.bookId | ||||||
|     this.episodeId = session.episodeId |     this.episodeId = session.episodeId | ||||||
|     this.mediaType = session.mediaType |     this.mediaType = session.mediaType | ||||||
|     this.duration = session.duration |     this.duration = session.duration | ||||||
|     this.playMethod = session.playMethod |     this.playMethod = session.playMethod | ||||||
|     this.mediaPlayer = session.mediaPlayer || null |     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.deviceInfo = new DeviceInfo(session.deviceInfo) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.serverVersion = session.serverVersion | ||||||
|     this.chapters = session.chapters || [] |     this.chapters = session.chapters || [] | ||||||
| 
 | 
 | ||||||
|     this.mediaMetadata = null |     this.mediaMetadata = null | ||||||
| @ -151,7 +166,7 @@ class PlaybackSession { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get deviceId() { |   get deviceId() { | ||||||
|     return this.deviceInfo?.deviceId |     return this.deviceInfo?.id | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get deviceDescription() { |   get deviceDescription() { | ||||||
| @ -169,10 +184,11 @@ class PlaybackSession { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { |   setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { | ||||||
|     this.id = getId('play') |     this.id = uuidv4() | ||||||
|     this.userId = user.id |     this.userId = user.id | ||||||
|     this.libraryId = libraryItem.libraryId |     this.libraryId = libraryItem.libraryId | ||||||
|     this.libraryItemId = libraryItem.id |     this.libraryItemId = libraryItem.id | ||||||
|  |     this.bookId = episodeId ? null : libraryItem.media.id | ||||||
|     this.episodeId = episodeId |     this.episodeId = episodeId | ||||||
|     this.mediaType = libraryItem.mediaType |     this.mediaType = libraryItem.mediaType | ||||||
|     this.mediaMetadata = libraryItem.media.metadata.clone() |     this.mediaMetadata = libraryItem.media.metadata.clone() | ||||||
| @ -189,6 +205,7 @@ class PlaybackSession { | |||||||
| 
 | 
 | ||||||
|     this.mediaPlayer = mediaPlayer |     this.mediaPlayer = mediaPlayer | ||||||
|     this.deviceInfo = deviceInfo || new DeviceInfo() |     this.deviceInfo = deviceInfo || new DeviceInfo() | ||||||
|  |     this.serverVersion = serverVersion | ||||||
| 
 | 
 | ||||||
|     this.timeListening = 0 |     this.timeListening = 0 | ||||||
|     this.startTime = startTime |     this.startTime = startTime | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| const Logger = require('../Logger') | const uuidv4 = require("uuid").v4 | ||||||
| const { getId } = require('../utils/index') |  | ||||||
| 
 | 
 | ||||||
| class Playlist { | class Playlist { | ||||||
|   constructor(playlist) { |   constructor(playlist) { | ||||||
| @ -88,7 +87,7 @@ class Playlist { | |||||||
|     if (!data.userId || !data.libraryId || !data.name) { |     if (!data.userId || !data.libraryId || !data.name) { | ||||||
|       return false |       return false | ||||||
|     } |     } | ||||||
|     this.id = getId('pl') |     this.id = uuidv4() | ||||||
|     this.userId = data.userId |     this.userId = data.userId | ||||||
|     this.libraryId = data.libraryId |     this.libraryId = data.libraryId | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const { getId } = require('../utils/index') | const uuidv4 = require("uuid").v4 | ||||||
| const { sanitizeFilename } = require('../utils/fileUtils') | const { sanitizeFilename } = require('../utils/fileUtils') | ||||||
| const globals = require('../utils/globals') | const globals = require('../utils/globals') | ||||||
| 
 | 
 | ||||||
| @ -70,7 +70,7 @@ class PodcastEpisodeDownload { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) { |   setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) { | ||||||
|     this.id = getId('epdl') |     this.id = uuidv4() | ||||||
|     this.podcastEpisode = podcastEpisode |     this.podcastEpisode = podcastEpisode | ||||||
| 
 | 
 | ||||||
|     const url = podcastEpisode.enclosure.url |     const url = podcastEpisode.enclosure.url | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const { getId } = require('../utils/index') | const uuidv4 = require("uuid").v4 | ||||||
| 
 | 
 | ||||||
| class Task { | class Task { | ||||||
|   constructor() { |   constructor() { | ||||||
| @ -35,7 +35,7 @@ class Task { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(action, title, description, showSuccess, data = {}) { |   setData(action, title, description, showSuccess, data = {}) { | ||||||
|     this.id = getId(action) |     this.id = uuidv4() | ||||||
|     this.action = action |     this.action = action | ||||||
|     this.data = { ...data } |     this.data = { ...data } | ||||||
|     this.title = title |     this.title = title | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| const Logger = require('../../Logger') | const Logger = require('../../Logger') | ||||||
| const { getId } = require('../../utils/index') | const uuidv4 = require("uuid").v4 | ||||||
| const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString') | const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString') | ||||||
| 
 | 
 | ||||||
| class Author { | class Author { | ||||||
| @ -53,7 +53,7 @@ class Author { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data) { |   setData(data) { | ||||||
|     this.id = getId('aut') |     this.id = uuidv4() | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|     this.description = data.description || null |     this.description = data.description || null | ||||||
|     this.asin = data.asin || null |     this.asin = data.asin || null | ||||||
|  | |||||||
| @ -1,12 +1,14 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const Logger = require('../../Logger') | 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 AudioFile = require('../files/AudioFile') | ||||||
| const AudioTrack = require('../files/AudioTrack') | const AudioTrack = require('../files/AudioTrack') | ||||||
| 
 | 
 | ||||||
| class PodcastEpisode { | class PodcastEpisode { | ||||||
|   constructor(episode) { |   constructor(episode) { | ||||||
|     this.libraryItemId = null |     this.libraryItemId = null | ||||||
|  |     this.podcastId = null | ||||||
|     this.id = null |     this.id = null | ||||||
|     this.index = null |     this.index = null | ||||||
| 
 | 
 | ||||||
| @ -32,6 +34,7 @@ class PodcastEpisode { | |||||||
| 
 | 
 | ||||||
|   construct(episode) { |   construct(episode) { | ||||||
|     this.libraryItemId = episode.libraryItemId |     this.libraryItemId = episode.libraryItemId | ||||||
|  |     this.podcastId = episode.podcastId | ||||||
|     this.id = episode.id |     this.id = episode.id | ||||||
|     this.index = episode.index |     this.index = episode.index | ||||||
|     this.season = episode.season |     this.season = episode.season | ||||||
| @ -54,6 +57,7 @@ class PodcastEpisode { | |||||||
|   toJSON() { |   toJSON() { | ||||||
|     return { |     return { | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|  |       podcastId: this.podcastId, | ||||||
|       id: this.id, |       id: this.id, | ||||||
|       index: this.index, |       index: this.index, | ||||||
|       season: this.season, |       season: this.season, | ||||||
| @ -75,6 +79,7 @@ class PodcastEpisode { | |||||||
|   toJSONExpanded() { |   toJSONExpanded() { | ||||||
|     return { |     return { | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|  |       podcastId: this.podcastId, | ||||||
|       id: this.id, |       id: this.id, | ||||||
|       index: this.index, |       index: this.index, | ||||||
|       season: this.season, |       season: this.season, | ||||||
| @ -117,7 +122,7 @@ class PodcastEpisode { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data, index = 1) { |   setData(data, index = 1) { | ||||||
|     this.id = getId('ep') |     this.id = uuidv4() | ||||||
|     this.index = index |     this.index = index | ||||||
|     this.title = data.title |     this.title = data.title | ||||||
|     this.subtitle = data.subtitle || '' |     this.subtitle = data.subtitle || '' | ||||||
| @ -133,7 +138,7 @@ class PodcastEpisode { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setDataFromAudioFile(audioFile, index) { |   setDataFromAudioFile(audioFile, index) { | ||||||
|     this.id = getId('ep') |     this.id = uuidv4() | ||||||
|     this.audioFile = audioFile |     this.audioFile = audioFile | ||||||
|     this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename)) |     this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename)) | ||||||
|     this.index = index |     this.index = index | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const { getId } = require('../../utils/index') | const uuidv4 = require("uuid").v4 | ||||||
| 
 | 
 | ||||||
| class Series { | class Series { | ||||||
|   constructor(series) { |   constructor(series) { | ||||||
| @ -40,7 +40,7 @@ class Series { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(data) { |   setData(data) { | ||||||
|     this.id = getId('ser') |     this.id = uuidv4() | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|     this.description = data.description || null |     this.description = data.description || null | ||||||
|     this.addedAt = Date.now() |     this.addedAt = Date.now() | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ const EBookFile = require('../files/EBookFile') | |||||||
| 
 | 
 | ||||||
| class Book { | class Book { | ||||||
|   constructor(book) { |   constructor(book) { | ||||||
|  |     this.id = null | ||||||
|     this.libraryItemId = null |     this.libraryItemId = null | ||||||
|     this.metadata = null |     this.metadata = null | ||||||
| 
 | 
 | ||||||
| @ -32,6 +33,7 @@ class Book { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   construct(book) { |   construct(book) { | ||||||
|  |     this.id = book.id | ||||||
|     this.libraryItemId = book.libraryItemId |     this.libraryItemId = book.libraryItemId | ||||||
|     this.metadata = new BookMetadata(book.metadata) |     this.metadata = new BookMetadata(book.metadata) | ||||||
|     this.coverPath = book.coverPath |     this.coverPath = book.coverPath | ||||||
| @ -46,6 +48,7 @@ class Book { | |||||||
| 
 | 
 | ||||||
|   toJSON() { |   toJSON() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       metadata: this.metadata.toJSON(), |       metadata: this.metadata.toJSON(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
| @ -59,6 +62,7 @@ class Book { | |||||||
| 
 | 
 | ||||||
|   toJSONMinified() { |   toJSONMinified() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       metadata: this.metadata.toJSONMinified(), |       metadata: this.metadata.toJSONMinified(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
|       tags: [...this.tags], |       tags: [...this.tags], | ||||||
| @ -75,6 +79,7 @@ class Book { | |||||||
| 
 | 
 | ||||||
|   toJSONExpanded() { |   toJSONExpanded() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       metadata: this.metadata.toJSONExpanded(), |       metadata: this.metadata.toJSONExpanded(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ const naturalSort = createNewSortInstance({ | |||||||
| 
 | 
 | ||||||
| class Podcast { | class Podcast { | ||||||
|   constructor(podcast) { |   constructor(podcast) { | ||||||
|  |     this.id = null | ||||||
|     this.libraryItemId = null |     this.libraryItemId = null | ||||||
|     this.metadata = null |     this.metadata = null | ||||||
|     this.coverPath = null |     this.coverPath = null | ||||||
| @ -32,6 +33,7 @@ class Podcast { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   construct(podcast) { |   construct(podcast) { | ||||||
|  |     this.id = podcast.id | ||||||
|     this.libraryItemId = podcast.libraryItemId |     this.libraryItemId = podcast.libraryItemId | ||||||
|     this.metadata = new PodcastMetadata(podcast.metadata) |     this.metadata = new PodcastMetadata(podcast.metadata) | ||||||
|     this.coverPath = podcast.coverPath |     this.coverPath = podcast.coverPath | ||||||
| @ -50,6 +52,7 @@ class Podcast { | |||||||
| 
 | 
 | ||||||
|   toJSON() { |   toJSON() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       metadata: this.metadata.toJSON(), |       metadata: this.metadata.toJSON(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
| @ -65,6 +68,7 @@ class Podcast { | |||||||
| 
 | 
 | ||||||
|   toJSONMinified() { |   toJSONMinified() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       metadata: this.metadata.toJSONMinified(), |       metadata: this.metadata.toJSONMinified(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
|       tags: [...this.tags], |       tags: [...this.tags], | ||||||
| @ -80,6 +84,7 @@ class Podcast { | |||||||
| 
 | 
 | ||||||
|   toJSONExpanded() { |   toJSONExpanded() { | ||||||
|     return { |     return { | ||||||
|  |       id: this.id, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       metadata: this.metadata.toJSONExpanded(), |       metadata: this.metadata.toJSONExpanded(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
| @ -284,8 +289,9 @@ class Podcast { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   addNewEpisodeFromAudioFile(audioFile, index) { |   addNewEpisodeFromAudioFile(audioFile, index) { | ||||||
|     var pe = new PodcastEpisode() |     const pe = new PodcastEpisode() | ||||||
|     pe.libraryItemId = this.libraryItemId |     pe.libraryItemId = this.libraryItemId | ||||||
|  |     pe.podcastId = this.id | ||||||
|     audioFile.index = 1 // Only 1 audio file per episode
 |     audioFile.index = 1 // Only 1 audio file per episode
 | ||||||
|     pe.setDataFromAudioFile(audioFile, index) |     pe.setDataFromAudioFile(audioFile, index) | ||||||
|     this.episodes.push(pe) |     this.episodes.push(pe) | ||||||
|  | |||||||
| @ -218,7 +218,7 @@ class BookMetadata { | |||||||
| 
 | 
 | ||||||
|   // Updates author name
 |   // Updates author name
 | ||||||
|   updateAuthor(updatedAuthor) { |   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 |     if (!author || author.name == updatedAuthor.name) return false | ||||||
|     author.name = updatedAuthor.name |     author.name = updatedAuthor.name | ||||||
|     return true |     return true | ||||||
|  | |||||||
| @ -1,9 +1,15 @@ | |||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
|  | 
 | ||||||
| class MediaProgress { | class MediaProgress { | ||||||
|   constructor(progress) { |   constructor(progress) { | ||||||
|     this.id = null |     this.id = null | ||||||
|  |     this.userId = null | ||||||
|     this.libraryItemId = null |     this.libraryItemId = null | ||||||
|     this.episodeId = null // For podcasts
 |     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.duration = null | ||||||
|     this.progress = null // 0 to 1
 |     this.progress = null // 0 to 1
 | ||||||
|     this.currentTime = null // seconds
 |     this.currentTime = null // seconds
 | ||||||
| @ -25,8 +31,11 @@ class MediaProgress { | |||||||
|   toJSON() { |   toJSON() { | ||||||
|     return { |     return { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|  |       userId: this.userId, | ||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       episodeId: this.episodeId, |       episodeId: this.episodeId, | ||||||
|  |       mediaItemId: this.mediaItemId, | ||||||
|  |       mediaItemType: this.mediaItemType, | ||||||
|       duration: this.duration, |       duration: this.duration, | ||||||
|       progress: this.progress, |       progress: this.progress, | ||||||
|       currentTime: this.currentTime, |       currentTime: this.currentTime, | ||||||
| @ -42,8 +51,11 @@ class MediaProgress { | |||||||
| 
 | 
 | ||||||
|   construct(progress) { |   construct(progress) { | ||||||
|     this.id = progress.id |     this.id = progress.id | ||||||
|  |     this.userId = progress.userId | ||||||
|     this.libraryItemId = progress.libraryItemId |     this.libraryItemId = progress.libraryItemId | ||||||
|     this.episodeId = progress.episodeId |     this.episodeId = progress.episodeId | ||||||
|  |     this.mediaItemId = progress.mediaItemId | ||||||
|  |     this.mediaItemType = progress.mediaItemType | ||||||
|     this.duration = progress.duration || 0 |     this.duration = progress.duration || 0 | ||||||
|     this.progress = progress.progress |     this.progress = progress.progress | ||||||
|     this.currentTime = progress.currentTime || 0 |     this.currentTime = progress.currentTime || 0 | ||||||
| @ -60,10 +72,16 @@ class MediaProgress { | |||||||
|     return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0)) |     return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(libraryItemId, progress, episodeId = null) { |   setData(libraryItem, progress, episodeId, userId) { | ||||||
|     this.id = episodeId ? `${libraryItemId}-${episodeId}` : libraryItemId |     this.id = uuidv4() | ||||||
|     this.libraryItemId = libraryItemId |     this.userId = userId | ||||||
|  |     this.libraryItemId = libraryItem.id | ||||||
|     this.episodeId = episodeId |     this.episodeId = episodeId | ||||||
|  | 
 | ||||||
|  |     // PodcastEpisodeId or BookId
 | ||||||
|  |     this.mediaItemId = episodeId || libraryItem.media.id | ||||||
|  |     this.mediaItemType = episodeId ? 'podcastEpisode' : 'book' | ||||||
|  | 
 | ||||||
|     this.duration = progress.duration || 0 |     this.duration = progress.duration || 0 | ||||||
|     this.progress = Math.min(1, (progress.progress || 0)) |     this.progress = Math.min(1, (progress.progress || 0)) | ||||||
|     this.currentTime = progress.currentTime || 0 |     this.currentTime = progress.currentTime || 0 | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ const MediaProgress = require('./MediaProgress') | |||||||
| class User { | class User { | ||||||
|   constructor(user) { |   constructor(user) { | ||||||
|     this.id = null |     this.id = null | ||||||
|  |     this.oldUserId = null // TODO: Temp for keeping old access tokens
 | ||||||
|     this.username = null |     this.username = null | ||||||
|     this.pash = null |     this.pash = null | ||||||
|     this.type = null |     this.type = null | ||||||
| @ -73,6 +74,7 @@ class User { | |||||||
|   toJSON() { |   toJSON() { | ||||||
|     return { |     return { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|  |       oldUserId: this.oldUserId, | ||||||
|       username: this.username, |       username: this.username, | ||||||
|       pash: this.pash, |       pash: this.pash, | ||||||
|       type: this.type, |       type: this.type, | ||||||
| @ -93,6 +95,7 @@ class User { | |||||||
|   toJSONForBrowser(hideRootToken = false, minimal = false) { |   toJSONForBrowser(hideRootToken = false, minimal = false) { | ||||||
|     const json = { |     const json = { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|  |       oldUserId: this.oldUserId, | ||||||
|       username: this.username, |       username: this.username, | ||||||
|       type: this.type, |       type: this.type, | ||||||
|       token: (this.type === 'root' && hideRootToken) ? '' : this.token, |       token: (this.type === 'root' && hideRootToken) ? '' : this.token, | ||||||
| @ -126,6 +129,7 @@ class User { | |||||||
|     } |     } | ||||||
|     return { |     return { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|  |       oldUserId: this.oldUserId, | ||||||
|       username: this.username, |       username: this.username, | ||||||
|       type: this.type, |       type: this.type, | ||||||
|       session, |       session, | ||||||
| @ -137,6 +141,7 @@ class User { | |||||||
| 
 | 
 | ||||||
|   construct(user) { |   construct(user) { | ||||||
|     this.id = user.id |     this.id = user.id | ||||||
|  |     this.oldUserId = user.oldUserId | ||||||
|     this.username = user.username |     this.username = user.username | ||||||
|     this.pash = user.pash |     this.pash = user.pash | ||||||
|     this.type = user.type |     this.type = user.type | ||||||
| @ -320,7 +325,7 @@ class User { | |||||||
|     if (!itemProgress) { |     if (!itemProgress) { | ||||||
|       const newItemProgress = new MediaProgress() |       const newItemProgress = new MediaProgress() | ||||||
| 
 | 
 | ||||||
|       newItemProgress.setData(libraryItem.id, updatePayload, episodeId) |       newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id) | ||||||
|       this.mediaProgress.push(newItemProgress) |       this.mediaProgress.push(newItemProgress) | ||||||
|       return true |       return true | ||||||
|     } |     } | ||||||
| @ -336,12 +341,6 @@ class User { | |||||||
|     return true |     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) { |   checkCanAccessLibrary(libraryId) { | ||||||
|     if (this.permissions.accessAllLibraries) return true |     if (this.permissions.accessAllLibraries) return true | ||||||
|     if (!this.librariesAccessible) return false |     if (!this.librariesAccessible) return false | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ const express = require('express') | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| 
 | 
 | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
| 
 | 
 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| @ -37,7 +38,6 @@ const Series = require('../objects/entities/Series') | |||||||
| 
 | 
 | ||||||
| class ApiRouter { | class ApiRouter { | ||||||
|   constructor(Server) { |   constructor(Server) { | ||||||
|     this.db = Server.db |  | ||||||
|     this.auth = Server.auth |     this.auth = Server.auth | ||||||
|     this.scanner = Server.scanner |     this.scanner = Server.scanner | ||||||
|     this.playbackSessionManager = Server.playbackSessionManager |     this.playbackSessionManager = Server.playbackSessionManager | ||||||
| @ -356,7 +356,7 @@ class ApiRouter { | |||||||
|     const json = user.toJSONForBrowser(hideRootToken) |     const json = user.toJSONForBrowser(hideRootToken) | ||||||
| 
 | 
 | ||||||
|     json.mediaProgress = json.mediaProgress.map(lip => { |     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) { |       if (!libraryItem) { | ||||||
|         Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId) |         Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId) | ||||||
|         lip.media = null |         lip.media = null | ||||||
| @ -381,11 +381,10 @@ class ApiRouter { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async handleDeleteLibraryItem(libraryItem) { |   async handleDeleteLibraryItem(libraryItem) { | ||||||
|     // Remove libraryItem from users
 |     // Remove media progress for this library item from all users
 | ||||||
|     for (let i = 0; i < this.db.users.length; i++) { |     for (const user of Database.users) { | ||||||
|       const user = this.db.users[i] |       for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) { | ||||||
|       if (user.removeMediaProgressForLibraryItem(libraryItem.id)) { |         await Database.removeMediaProgress(mediaProgress.id) | ||||||
|         await this.db.updateEntity('user', user) |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -393,12 +392,12 @@ class ApiRouter { | |||||||
| 
 | 
 | ||||||
|     if (libraryItem.isBook) { |     if (libraryItem.isBook) { | ||||||
|       // remove book from collections
 |       // 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++) { |       for (let i = 0; i < collectionsWithBook.length; i++) { | ||||||
|         const collection = collectionsWithBook[i] |         const collection = collectionsWithBook[i] | ||||||
|         collection.removeBook(libraryItem.id) |         collection.removeBook(libraryItem.id) | ||||||
|         await this.db.updateEntity('collection', collection) |         await Database.removeCollectionBook(collection.id, libraryItem.media.id) | ||||||
|         SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems)) |         SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems)) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // Check remove empty series
 |       // Check remove empty series
 | ||||||
| @ -406,7 +405,7 @@ class ApiRouter { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // remove item from playlists
 |     // 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++) { |     for (let i = 0; i < playlistsWithItem.length; i++) { | ||||||
|       const playlist = playlistsWithItem[i] |       const playlist = playlistsWithItem[i] | ||||||
|       playlist.removeItemsForLibraryItem(libraryItem.id) |       playlist.removeItemsForLibraryItem(libraryItem.id) | ||||||
| @ -414,11 +413,12 @@ class ApiRouter { | |||||||
|       // If playlist is now empty then remove it
 |       // If playlist is now empty then remove it
 | ||||||
|       if (!playlist.items.length) { |       if (!playlist.items.length) { | ||||||
|         Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`) |         Logger.info(`[ApiRouter] 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', playlist.toJSONExpanded(this.db.libraryItems)) |         SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems)) | ||||||
|       } else { |       } else { | ||||||
|         await this.db.updateEntity('playlist', playlist) |         await Database.updatePlaylist(playlist) | ||||||
|         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(this.db.libraryItems)) | 
 | ||||||
|  |         SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems)) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -436,7 +436,7 @@ class ApiRouter { | |||||||
|       await fs.remove(itemMetadataPath) |       await fs.remove(itemMetadataPath) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     await this.db.removeLibraryItem(libraryItem.id) |     await Database.removeLibraryItem(libraryItem.id) | ||||||
|     SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded()) |     SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded()) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -444,27 +444,27 @@ class ApiRouter { | |||||||
|     if (!seriesToCheck || !seriesToCheck.length) return |     if (!seriesToCheck || !seriesToCheck.length) return | ||||||
| 
 | 
 | ||||||
|     for (const series of seriesToCheck) { |     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) { |       if (!otherLibraryItemsInSeries.length) { | ||||||
|         // Close open RSS feed for series
 |         // Close open RSS feed for series
 | ||||||
|         await this.rssFeedManager.closeFeedForEntityId(series.id) |         await this.rssFeedManager.closeFeedForEntityId(series.id) | ||||||
|         Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) |         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?
 |         // TODO: Socket events for series?
 | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getUserListeningSessionsHelper(userId) { |   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) |     return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getAllSessionsWithUserData() { |   async getAllSessionsWithUserData() { | ||||||
|     const sessions = await this.db.getAllSessions() |     const sessions = await Database.getPlaybackSessions() | ||||||
|     sessions.sort((a, b) => b.updatedAt - a.updatedAt) |     sessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||||
|     return sessions.map(se => { |     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 { |       return { | ||||||
|         ...se, |         ...se, | ||||||
|         user: user ? { id: user.id, username: user.username } : null |         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')) { |           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) { |             if (!author) { | ||||||
|               author = new Author() |               author = new Author() | ||||||
|               author.setData(mediaMetadata.authors[i]) |               author.setData(mediaMetadata.authors[i]) | ||||||
| @ -546,7 +546,7 @@ class ApiRouter { | |||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         if (newAuthors.length) { |         if (newAuthors.length) { | ||||||
|           await this.db.insertEntities('author', newAuthors) |           await Database.createBulkAuthors(newAuthors) | ||||||
|           SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) |           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')) { |           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) { |             if (!seriesItem) { | ||||||
|               seriesItem = new Series() |               seriesItem = new Series() | ||||||
|               seriesItem.setData(mediaMetadata.series[i]) |               seriesItem.setData(mediaMetadata.series[i]) | ||||||
| @ -575,7 +575,7 @@ class ApiRouter { | |||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         if (newSeries.length) { |         if (newSeries.length) { | ||||||
|           await this.db.insertEntities('series', newSeries) |           await Database.createBulkSeries(newSeries) | ||||||
|           SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) |           SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -8,8 +8,7 @@ const fs = require('../libs/fsExtra') | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class HlsRouter { | class HlsRouter { | ||||||
|   constructor(db, auth, playbackSessionManager) { |   constructor(auth, playbackSessionManager) { | ||||||
|     this.db = db |  | ||||||
|     this.auth = auth |     this.auth = auth | ||||||
|     this.playbackSessionManager = playbackSessionManager |     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 Path = require('path') | ||||||
|  | const uuidv4 = require("uuid").v4 | ||||||
| const fs = require('../libs/fsExtra') | const fs = require('../libs/fsExtra') | ||||||
| const date = require('../libs/dateAndTime') | const date = require('../libs/dateAndTime') | ||||||
| 
 | 
 | ||||||
| @ -6,7 +7,7 @@ const Logger = require('../Logger') | |||||||
| const Library = require('../objects/Library') | const Library = require('../objects/Library') | ||||||
| const { LogLevel } = require('../utils/constants') | const { LogLevel } = require('../utils/constants') | ||||||
| const filePerms = require('../utils/filePerms') | const filePerms = require('../utils/filePerms') | ||||||
| const { getId, secondsToTimestamp } = require('../utils/index') | const { secondsToTimestamp } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
| class LibraryScan { | class LibraryScan { | ||||||
|   constructor() { |   constructor() { | ||||||
| @ -84,7 +85,7 @@ class LibraryScan { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setData(library, scanOptions, type = 'scan') { |   setData(library, scanOptions, type = 'scan') { | ||||||
|     this.id = getId('lscan') |     this.id = uuidv4() | ||||||
|     this.type = type |     this.type = type | ||||||
|     this.library = new Library(library.toJSON()) // clone library
 |     this.library = new Library(library.toJSON()) // clone library
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ const fs = require('../libs/fsExtra') | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const SocketAuthority = require('../SocketAuthority') | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
| 
 | 
 | ||||||
| // Utils
 | // Utils
 | ||||||
| const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir') | const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir') | ||||||
| @ -22,8 +23,7 @@ const Series = require('../objects/entities/Series') | |||||||
| const Task = require('../objects/Task') | const Task = require('../objects/Task') | ||||||
| 
 | 
 | ||||||
| class Scanner { | class Scanner { | ||||||
|   constructor(db, coverManager, taskManager) { |   constructor(coverManager, taskManager) { | ||||||
|     this.db = db |  | ||||||
|     this.coverManager = coverManager |     this.coverManager = coverManager | ||||||
|     this.taskManager = taskManager |     this.taskManager = taskManager | ||||||
| 
 | 
 | ||||||
| @ -66,7 +66,7 @@ class Scanner { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async scanLibraryItemByRequest(libraryItem) { |   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) { |     if (!library) { | ||||||
|       Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) |       Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) | ||||||
|       return ScanResult.NOTHING |       return ScanResult.NOTHING | ||||||
| @ -108,7 +108,7 @@ class Scanner { | |||||||
|     if (checkRes.updated) hasUpdated = true |     if (checkRes.updated) hasUpdated = true | ||||||
| 
 | 
 | ||||||
|     // Sync other files first so that local images are used as cover art
 |     // 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 |       hasUpdated = true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -141,7 +141,7 @@ class Scanner { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (hasUpdated) { |     if (hasUpdated) { | ||||||
|       await this.db.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|       return ScanResult.UPDATED |       return ScanResult.UPDATED | ||||||
|     } |     } | ||||||
| @ -160,7 +160,7 @@ class Scanner { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const scanOptions = new ScanOptions() |     const scanOptions = new ScanOptions() | ||||||
|     scanOptions.setData(options, this.db.serverSettings) |     scanOptions.setData(options, Database.serverSettings) | ||||||
| 
 | 
 | ||||||
|     const libraryScan = new LibraryScan() |     const libraryScan = new LibraryScan() | ||||||
|     libraryScan.setData(library, scanOptions) |     libraryScan.setData(library, scanOptions) | ||||||
| @ -212,7 +212,7 @@ class Scanner { | |||||||
| 
 | 
 | ||||||
|     // Remove items with no inode
 |     // Remove items with no inode
 | ||||||
|     libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino) |     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 MaxSizePerChunk = 2.5e9 | ||||||
|     const itemDataToRescanChunks = [] |     const itemDataToRescanChunks = [] | ||||||
| @ -333,7 +333,7 @@ class Scanner { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async updateLibraryItemChunk(itemsToUpdate) { |   async updateLibraryItemChunk(itemsToUpdate) { | ||||||
|     await this.db.updateLibraryItems(itemsToUpdate) |     await Database.updateBulkLibraryItems(itemsToUpdate) | ||||||
|     SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded())) |     SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded())) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -351,7 +351,7 @@ class Scanner { | |||||||
| 
 | 
 | ||||||
|     if (itemsUpdated.length) { |     if (itemsUpdated.length) { | ||||||
|       libraryScan.resultsUpdated += itemsUpdated.length |       libraryScan.resultsUpdated += itemsUpdated.length | ||||||
|       await this.db.updateLibraryItems(itemsUpdated) |       await Database.updateBulkLibraryItems(itemsUpdated) | ||||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) |       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -368,7 +368,7 @@ class Scanner { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     libraryScan.resultsAdded += newLibraryItems.length |     libraryScan.resultsAdded += newLibraryItems.length | ||||||
|     await this.db.insertLibraryItems(newLibraryItems) |     await Database.createBulkLibraryItems(newLibraryItems) | ||||||
|     SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded())) |     SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded())) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -436,6 +436,7 @@ class Scanner { | |||||||
| 
 | 
 | ||||||
|     const libraryItem = new LibraryItem() |     const libraryItem = new LibraryItem() | ||||||
|     libraryItem.setData(library.mediaType, libraryItemData) |     libraryItem.setData(library.mediaType, libraryItemData) | ||||||
|  |     libraryItem.setLastScan() | ||||||
| 
 | 
 | ||||||
|     const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video') |     const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video') | ||||||
|     if (mediaFiles.length) { |     if (mediaFiles.length) { | ||||||
| @ -478,7 +479,7 @@ class Scanner { | |||||||
|     if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) { |     if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) { | ||||||
|       var newAuthors = [] |       var newAuthors = [] | ||||||
|       libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => { |       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) _author = newAuthors.find(au => au.checkNameEquals(tempMinAuthor.name)) // Check new unsaved authors
 | ||||||
|         if (!_author) { // Must create new author
 |         if (!_author) { // Must create new author
 | ||||||
|           _author = new Author() |           _author = new Author() | ||||||
| @ -492,14 +493,14 @@ class Scanner { | |||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       if (newAuthors.length) { |       if (newAuthors.length) { | ||||||
|         await this.db.insertEntities('author', newAuthors) |         await Database.createBulkAuthors(newAuthors) | ||||||
|         SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) |         SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON())) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) { |     if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) { | ||||||
|       var newSeries = [] |       var newSeries = [] | ||||||
|       libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => { |       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) _series = newSeries.find(se => se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series
 | ||||||
|         if (!_series) { // Must create new series
 |         if (!_series) { // Must create new series
 | ||||||
|           _series = new Series() |           _series = new Series() | ||||||
| @ -513,7 +514,7 @@ class Scanner { | |||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       if (newSeries.length) { |       if (newSeries.length) { | ||||||
|         await this.db.insertEntities('series', newSeries) |         await Database.createBulkSeries(newSeries) | ||||||
|         SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) |         SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON())) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -551,7 +552,7 @@ class Scanner { | |||||||
| 
 | 
 | ||||||
|     for (const folderId in folderGroups) { |     for (const folderId in folderGroups) { | ||||||
|       const libraryId = folderGroups[folderId].libraryId |       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) { |       if (!library) { | ||||||
|         Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) |         Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) | ||||||
|         continue; |         continue; | ||||||
| @ -597,12 +598,12 @@ class Scanner { | |||||||
|       const altDir = `${itemDir}/${firstNest}` |       const altDir = `${itemDir}/${firstNest}` | ||||||
| 
 | 
 | ||||||
|       const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) |       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) { |       if (!childLibraryItem) { | ||||||
|         continue |         continue | ||||||
|       } |       } | ||||||
|       const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir) |       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) { |       if (altChildLibraryItem) { | ||||||
|         continue |         continue | ||||||
|       } |       } | ||||||
| @ -619,9 +620,9 @@ class Scanner { | |||||||
|       const dirIno = await getIno(fullPath) |       const dirIno = await getIno(fullPath) | ||||||
| 
 | 
 | ||||||
|       // Check if book dir group is already an item
 |       // 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) { |       if (!existingLibraryItem) { | ||||||
|         existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno) |         existingLibraryItem = Database.libraryItems.find(li => li.ino === dirIno) | ||||||
|         if (existingLibraryItem) { |         if (existingLibraryItem) { | ||||||
|           Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`) |           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
 |           // 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) { |           if (!exists) { | ||||||
|             Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`) |             Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`) | ||||||
|             existingLibraryItem.setMissing() |             existingLibraryItem.setMissing() | ||||||
|             await this.db.updateLibraryItem(existingLibraryItem) |             await Database.updateLibraryItem(existingLibraryItem) | ||||||
|             SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded()) |             SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded()) | ||||||
| 
 | 
 | ||||||
|             itemGroupingResults[itemDir] = ScanResult.REMOVED |             itemGroupingResults[itemDir] = ScanResult.REMOVED | ||||||
| @ -654,7 +655,7 @@ class Scanner { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // Check if a library item is a subdirectory of this dir
 |       // 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) { |       if (childItem) { | ||||||
|         Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`) |         Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`) | ||||||
|         itemGroupingResults[itemDir] = ScanResult.NOTHING |         itemGroupingResults[itemDir] = ScanResult.NOTHING | ||||||
| @ -666,7 +667,7 @@ class Scanner { | |||||||
|       var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem) |       var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem) | ||||||
|       if (newLibraryItem) { |       if (newLibraryItem) { | ||||||
|         await this.createNewAuthorsAndSeries(newLibraryItem) |         await this.createNewAuthorsAndSeries(newLibraryItem) | ||||||
|         await this.db.insertLibraryItem(newLibraryItem) |         await Database.createLibraryItem(newLibraryItem) | ||||||
|         SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded()) |         SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded()) | ||||||
|       } |       } | ||||||
|       itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING |       itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING | ||||||
| @ -686,7 +687,7 @@ class Scanner { | |||||||
|       titleDistance: 2, |       titleDistance: 2, | ||||||
|       authorDistance: 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) |     const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options) | ||||||
|     if (results.length) { |     if (results.length) { | ||||||
|       if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`) |       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 
 |     // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and 
 | ||||||
|     // the overrideDefaults option is not set or set to false.
 |     // 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.overrideCover = true | ||||||
|       options.overrideDetails = true |       options.overrideDetails = true | ||||||
|     } |     } | ||||||
| @ -783,7 +784,7 @@ class Scanner { | |||||||
|         await this.quickMatchPodcastEpisodes(libraryItem, options) |         await this.quickMatchPodcastEpisodes(libraryItem, options) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       await this.db.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -878,11 +879,11 @@ class Scanner { | |||||||
|       const authorPayload = [] |       const authorPayload = [] | ||||||
|       for (let index = 0; index < matchData.author.length; index++) { |       for (let index = 0; index < matchData.author.length; index++) { | ||||||
|         const authorName = matchData.author[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) { |         if (!author) { | ||||||
|           author = new Author() |           author = new Author() | ||||||
|           author.setData({ name: authorName }) |           author.setData({ name: authorName }) | ||||||
|           await this.db.insertEntity('author', author) |           await Database.createAuthor(author) | ||||||
|           SocketAuthority.emitter('author_added', author.toJSON()) |           SocketAuthority.emitter('author_added', author.toJSON()) | ||||||
|         } |         } | ||||||
|         authorPayload.push(author.toJSONMinimal()) |         authorPayload.push(author.toJSONMinimal()) | ||||||
| @ -896,11 +897,11 @@ class Scanner { | |||||||
|       const seriesPayload = [] |       const seriesPayload = [] | ||||||
|       for (let index = 0; index < matchData.series.length; index++) { |       for (let index = 0; index < matchData.series.length; index++) { | ||||||
|         const seriesMatchItem = matchData.series[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) { |         if (!seriesItem) { | ||||||
|           seriesItem = new Series() |           seriesItem = new Series() | ||||||
|           seriesItem.setData({ name: seriesMatchItem.series }) |           seriesItem.setData({ name: seriesMatchItem.series }) | ||||||
|           await this.db.insertEntity('series', seriesItem) |           await Database.createSeries(seriesItem) | ||||||
|           SocketAuthority.emitter('series_added', seriesItem.toJSON()) |           SocketAuthority.emitter('series_added', seriesItem.toJSON()) | ||||||
|         } |         } | ||||||
|         seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) |         seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) | ||||||
| @ -981,7 +982,7 @@ class Scanner { | |||||||
|       return |       return | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var itemsInLibrary = this.db.getLibraryItemsInLibrary(library.id) |     const itemsInLibrary = Database.libraryItems.filter(li => li.libraryId === library.id) | ||||||
|     if (!itemsInLibrary.length) { |     if (!itemsInLibrary.length) { | ||||||
|       Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) |       Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) | ||||||
|       return |       return | ||||||
|  | |||||||
| @ -17,24 +17,31 @@ | |||||||
|  @param value2 Other item to compare |  @param value2 Other item to compare | ||||||
|  @param stack Used internally to track circular refs - don't set it |  @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.
 |   // Numbers, strings, null, undefined, symbols, functions, booleans.
 | ||||||
|   // Also: objects (incl. arrays) that are actually the same instance
 |   // Also: objects (incl. arrays) that are actually the same instance
 | ||||||
|   if (value1 === value2) { |   if (value1 === value2) { | ||||||
|     // Fast and done
 |     // Fast and done
 | ||||||
|     return true; |     return true | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Truthy check to handle value1=null, value2=Object
 |   // Truthy check to handle value1=null, value2=Object
 | ||||||
|   if ((value1 && !value2) || (!value1 && value2)) { |   if ((value1 && !value2) || (!value1 && value2)) { | ||||||
|  |     console.log('value1/value2 falsy mismatch', value1, value2) | ||||||
|     return false |     return false | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const type1 = typeof value1; |   const type1 = typeof value1 | ||||||
| 
 | 
 | ||||||
|   // Ensure types match
 |   // Ensure types match
 | ||||||
|   if (type1 !== typeof value2) { |   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
 |   // 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
 |     // Failed initial equals test, but could still have equivalent
 | ||||||
|     // implementations - note, will match on functions that have same name
 |     // implementations - note, will match on functions that have same name
 | ||||||
|     // and are native code: `function abc() { [native code] }`
 |     // 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
 |   // For these types, cannot still be equal at this point, so fast-fail
 | ||||||
|   if (type1 === 'bigint' || type1 === 'boolean' || |   if (type1 === 'bigint' || type1 === 'boolean' || | ||||||
|     type1 === 'function' || type1 === 'string' || |     type1 === 'function' || type1 === 'string' || | ||||||
|     type1 === 'symbol') { |     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
 |   // 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)
 |   // exact instance then we're not here - that was checked above)
 | ||||||
|   if (value1 instanceof Date) { |   if (value1 instanceof Date) { | ||||||
|     if (!(value2 instanceof Date)) { |     if (!(value2 instanceof Date)) { | ||||||
|       return false; |       return false | ||||||
|     } |     } | ||||||
|     // Convert to number to compare
 |     // Convert to number to compare
 | ||||||
|     const asNum1 = +value1, asNum2 = +value2; |     const asNum1 = +value1, asNum2 = +value2 | ||||||
|     // Check if both invalid (NaN) or are same value
 |     // 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
 |   // 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
 |   // breadcrumb
 | ||||||
|   stack.push(value1); |   stack.push(value1) | ||||||
| 
 | 
 | ||||||
|   // Handle arrays
 |   // Handle arrays
 | ||||||
|   if (Array.isArray(value1)) { |   if (Array.isArray(value1)) { | ||||||
|     if (!Array.isArray(value2)) { |     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) { |     if (length !== value2.length) { | ||||||
|       return false; |       console.log('array length diff', length) | ||||||
|  |       return false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     for (let i = 0; i < length; i++) { |     for (let i = 0; i < length; i++) { | ||||||
|       if (!areEquivalent(value1[i], value2[i], stack)) { |       if (!areEquivalent(value1[i], value2[i], numToString, stack)) { | ||||||
|         return false; |         console.log('2 array items are not equiv', value1[i], value2[i]) | ||||||
|  |         return false | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return true; |     return true | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Final case: object
 |   // Final case: object
 | ||||||
| 
 | 
 | ||||||
|   // get both key lists and check length
 |   // get both key lists and check length
 | ||||||
|   const keys1 = Object.keys(value1); |   const keys1 = Object.keys(value1) | ||||||
|   const keys2 = Object.keys(value2); |   const keys2 = Object.keys(value2) | ||||||
|   const numKeys = keys1.length; |   const numKeys = keys1.length | ||||||
| 
 | 
 | ||||||
|   if (keys2.length !== numKeys) { |   if (keys2.length !== numKeys) { | ||||||
|     return false; |     console.log('Key length is diff', keys2.length, numKeys) | ||||||
|  |     return false | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Empty object on both sides?
 |   // Empty object on both sides?
 | ||||||
|   if (numKeys === 0) { |   if (numKeys === 0) { | ||||||
|     return true; |     return true | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // sort is a native call so it's very fast - much faster than comparing the
 |   // 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
 |   // values at each key if it can be avoided, so do the sort and then
 | ||||||
|   // ensure every key matches at every index
 |   // ensure every key matches at every index
 | ||||||
|   keys1.sort(); |   keys1.sort() | ||||||
|   keys2.sort(); |   keys2.sort() | ||||||
| 
 | 
 | ||||||
|   // Ensure perfect match across all keys
 |   // Ensure perfect match across all keys
 | ||||||
|   for (let i = 0; i < numKeys; i++) { |   for (let i = 0; i < numKeys; i++) { | ||||||
|     if (keys1[i] !== keys2[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
 |   // Ensure perfect match across all values
 | ||||||
|   for (let i = 0; i < numKeys; i++) { |   for (let i = 0; i < numKeys; i++) { | ||||||
|     if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) { |     if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) { | ||||||
|       return false; |       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 { sort, createNewSortInstance } = require('../libs/fastSort') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const Database = require('../Database') | ||||||
| const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index') | const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index') | ||||||
| const naturalSort = createNewSortInstance({ | const naturalSort = createNewSortInstance({ | ||||||
|   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare |   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare | ||||||
| @ -574,7 +575,7 @@ module.exports = { | |||||||
|             const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id) |             const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id) | ||||||
| 
 | 
 | ||||||
|             if (!seriesMap[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) { |               if (seriesObj) { | ||||||
|                 const series = { |                 const series = { | ||||||
|                   ...seriesObj.toJSON(), |                   ...seriesObj.toJSON(), | ||||||
| @ -626,7 +627,7 @@ module.exports = { | |||||||
|         if (libraryItem.media.metadata.authors.length) { |         if (libraryItem.media.metadata.authors.length) { | ||||||
|           for (const libraryAuthor of libraryItem.media.metadata.authors) { |           for (const libraryAuthor of libraryItem.media.metadata.authors) { | ||||||
|             if (!authorMap[libraryAuthor.id]) { |             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) { |               if (authorObj) { | ||||||
|                 const author = { |                 const author = { | ||||||
|                   ...authorObj.toJSON(), |                   ...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