mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:Auth to use new user model
- Express requests include userNew to start migrating API controllers to new user model
This commit is contained in:
		
							parent
							
								
									59370cae81
								
							
						
					
					
						commit
						202ceb02b5
					
				@ -157,10 +157,6 @@ export default {
 | 
				
			|||||||
    this.init()
 | 
					    this.init()
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeDestroy() {
 | 
					  beforeDestroy() {
 | 
				
			||||||
    if (this.$refs.accountModal) {
 | 
					 | 
				
			||||||
      this.$refs.accountModal.close()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.$root.socket) {
 | 
					    if (this.$root.socket) {
 | 
				
			||||||
      this.$root.socket.off('user_added', this.addUpdateUser)
 | 
					      this.$root.socket.off('user_added', this.addUpdateUser)
 | 
				
			||||||
      this.$root.socket.off('user_updated', this.addUpdateUser)
 | 
					      this.$root.socket.off('user_updated', this.addUpdateUser)
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,11 @@ export default {
 | 
				
			|||||||
      this.showAccountModal = true
 | 
					      this.showAccountModal = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {}
 | 
					  mounted() {},
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    if (this.$refs.accountModal) {
 | 
				
			||||||
 | 
					      this.$refs.accountModal.close()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@ -16,7 +16,7 @@ export const state = () => ({
 | 
				
			|||||||
    authorSortBy: 'name',
 | 
					    authorSortBy: 'name',
 | 
				
			||||||
    authorSortDesc: false,
 | 
					    authorSortDesc: false,
 | 
				
			||||||
    jumpForwardAmount: 10,
 | 
					    jumpForwardAmount: 10,
 | 
				
			||||||
    jumpBackwardAmount: 10,
 | 
					    jumpBackwardAmount: 10
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -26,7 +26,9 @@ export const getters = {
 | 
				
			|||||||
  getToken: (state) => {
 | 
					  getToken: (state) => {
 | 
				
			||||||
    return state.user?.token || null
 | 
					    return state.user?.token || null
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
 | 
					  getUserMediaProgress:
 | 
				
			||||||
 | 
					    (state) =>
 | 
				
			||||||
 | 
					    (libraryItemId, episodeId = null) => {
 | 
				
			||||||
      if (!state.user.mediaProgress) return null
 | 
					      if (!state.user.mediaProgress) return null
 | 
				
			||||||
      return state.user.mediaProgress.find((li) => {
 | 
					      return state.user.mediaProgress.find((li) => {
 | 
				
			||||||
        if (episodeId && li.episodeId !== episodeId) return false
 | 
					        if (episodeId && li.episodeId !== episodeId) return false
 | 
				
			||||||
@ -153,7 +155,7 @@ export const mutations = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  setUserToken(state, token) {
 | 
					  setUserToken(state, token) {
 | 
				
			||||||
    state.user.token = token
 | 
					    state.user.token = token
 | 
				
			||||||
    localStorage.setItem('token', user.token)
 | 
					    localStorage.setItem('token', token)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  updateMediaProgress(state, { id, data }) {
 | 
					  updateMediaProgress(state, { id, data }) {
 | 
				
			||||||
    if (!state.user) return
 | 
					    if (!state.user) return
 | 
				
			||||||
 | 
				
			|||||||
@ -213,8 +213,11 @@ class Auth {
 | 
				
			|||||||
        return null
 | 
					        return null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      user.authOpenIDSub = userinfo.sub
 | 
					      // Update user with OpenID sub
 | 
				
			||||||
      await Database.userModel.updateFromOld(user)
 | 
					      if (!user.extraData) user.extraData = {}
 | 
				
			||||||
 | 
					      user.extraData.authOpenIDSub = userinfo.sub
 | 
				
			||||||
 | 
					      user.changed('extraData', true)
 | 
				
			||||||
 | 
					      await user.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Logger.debug(`[Auth] openid: User found by email/username`)
 | 
					      Logger.debug(`[Auth] openid: User found by email/username`)
 | 
				
			||||||
      return user
 | 
					      return user
 | 
				
			||||||
@ -788,12 +791,14 @@ class Auth {
 | 
				
			|||||||
    await Database.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
 | 
				
			||||||
    const users = await Database.userModel.getOldUsers()
 | 
					    const users = await Database.userModel.findAll({
 | 
				
			||||||
 | 
					      attributes: ['id', 'username', 'token']
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    if (users.length) {
 | 
					    if (users.length) {
 | 
				
			||||||
      for (const user of users) {
 | 
					      for (const user of users) {
 | 
				
			||||||
        user.token = await this.generateAccessToken(user)
 | 
					        user.token = await this.generateAccessToken(user)
 | 
				
			||||||
 | 
					        await user.save({ hooks: false })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      await Database.updateBulkUsers(users)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -879,13 +884,13 @@ class Auth {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Return the login info payload for a user
 | 
					   * Return the login info payload for a user
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {Object} user
 | 
					   * @param {import('./models/User')} user
 | 
				
			||||||
   * @returns {Promise<Object>} jsonPayload
 | 
					   * @returns {Promise<Object>} jsonPayload
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async getUserLoginResponsePayload(user) {
 | 
					  async getUserLoginResponsePayload(user) {
 | 
				
			||||||
    const libraryIds = await Database.libraryModel.getAllLibraryIds()
 | 
					    const libraryIds = await Database.libraryModel.getAllLibraryIds()
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      user: user.toJSONForBrowser(),
 | 
					      user: user.toOldJSONForBrowser(),
 | 
				
			||||||
      userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
 | 
					      userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
 | 
				
			||||||
      serverSettings: Database.serverSettings.toJSONForBrowser(),
 | 
					      serverSettings: Database.serverSettings.toJSONForBrowser(),
 | 
				
			||||||
      ereaderDevices: Database.emailSettings.getEReaderDevices(user),
 | 
					      ereaderDevices: Database.emailSettings.getEReaderDevices(user),
 | 
				
			||||||
@ -907,6 +912,7 @@ class Auth {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * User changes their password from request
 | 
					   * User changes their password from request
 | 
				
			||||||
 | 
					   * TODO: Update responses to use error status codes
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('express').Request} req
 | 
					   * @param {import('express').Request} req
 | 
				
			||||||
   * @param {import('express').Response} res
 | 
					   * @param {import('express').Response} res
 | 
				
			||||||
@ -941,19 +947,27 @@ class Auth {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    matchingUser.pash = pw
 | 
					    Database.userModel
 | 
				
			||||||
 | 
					      .update(
 | 
				
			||||||
    const success = await Database.updateUser(matchingUser)
 | 
					        {
 | 
				
			||||||
    if (success) {
 | 
					          pash: pw
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          where: { id: matchingUser.id }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
        Logger.info(`[Auth] User "${matchingUser.username}" changed password`)
 | 
					        Logger.info(`[Auth] User "${matchingUser.username}" changed password`)
 | 
				
			||||||
        res.json({
 | 
					        res.json({
 | 
				
			||||||
          success: true
 | 
					          success: true
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    } else {
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((error) => {
 | 
				
			||||||
 | 
					        Logger.error(`[Auth] User "${matchingUser.username}" failed to change password`, error)
 | 
				
			||||||
        res.json({
 | 
					        res.json({
 | 
				
			||||||
          error: 'Unknown error'
 | 
					          error: 'Unknown error'
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					      })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -363,7 +363,7 @@ class Database {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  async createRootUser(username, pash, auth) {
 | 
					  async createRootUser(username, pash, auth) {
 | 
				
			||||||
    if (!this.sequelize) return false
 | 
					    if (!this.sequelize) return false
 | 
				
			||||||
    await this.models.user.createRootUser(username, pash, auth)
 | 
					    await this.userModel.createRootUser(username, pash, auth)
 | 
				
			||||||
    this.hasRootUser = true
 | 
					    this.hasRootUser = true
 | 
				
			||||||
    return true
 | 
					    return true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -390,11 +390,6 @@ class Database {
 | 
				
			|||||||
    return this.models.user.updateFromOld(oldUser)
 | 
					    return this.models.user.updateFromOld(oldUser)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateBulkUsers(oldUsers) {
 | 
					 | 
				
			||||||
    if (!this.sequelize) return false
 | 
					 | 
				
			||||||
    return Promise.all(oldUsers.map((u) => this.updateUser(u)))
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  removeUser(userId) {
 | 
					  removeUser(userId) {
 | 
				
			||||||
    if (!this.sequelize) return false
 | 
					    if (!this.sequelize) return false
 | 
				
			||||||
    return this.models.user.removeById(userId)
 | 
					    return this.models.user.removeById(userId)
 | 
				
			||||||
 | 
				
			|||||||
@ -89,9 +89,25 @@ class Server {
 | 
				
			|||||||
    this.io = null
 | 
					    this.io = null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Middleware to check if the current request is authenticated
 | 
				
			||||||
 | 
					   * req.user is set if authenticated to the OLD user object
 | 
				
			||||||
 | 
					   * req.userNew is set if authenticated to the NEW user object
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('express').Request} req
 | 
				
			||||||
 | 
					   * @param {import('express').Response} res
 | 
				
			||||||
 | 
					   * @param {import('express').NextFunction} next
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  authMiddleware(req, res, next) {
 | 
					  authMiddleware(req, res, next) {
 | 
				
			||||||
    // ask passportjs if the current request is authenticated
 | 
					    // ask passportjs if the current request is authenticated
 | 
				
			||||||
    this.auth.isAuthenticated(req, res, next)
 | 
					    this.auth.isAuthenticated(req, res, () => {
 | 
				
			||||||
 | 
					      if (req.user) {
 | 
				
			||||||
 | 
					        // TODO: req.userNew to become req.user
 | 
				
			||||||
 | 
					        req.userNew = req.user
 | 
				
			||||||
 | 
					        req.user = Database.userModel.getOldUser(req.user)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      next()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cancelLibraryScan(libraryId) {
 | 
					  cancelLibraryScan(libraryId) {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,11 +3,20 @@ const Logger = require('./Logger')
 | 
				
			|||||||
const Database = require('./Database')
 | 
					const Database = require('./Database')
 | 
				
			||||||
const Auth = require('./Auth')
 | 
					const Auth = require('./Auth')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef SocketClient
 | 
				
			||||||
 | 
					 * @property {string} id socket id
 | 
				
			||||||
 | 
					 * @property {SocketIO.Socket} socket
 | 
				
			||||||
 | 
					 * @property {number} connected_at
 | 
				
			||||||
 | 
					 * @property {import('./models/User')} user
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SocketAuthority {
 | 
					class SocketAuthority {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    this.Server = null
 | 
					    this.Server = null
 | 
				
			||||||
    this.io = null
 | 
					    this.io = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @type {Object.<string, SocketClient>} */
 | 
				
			||||||
    this.clients = {}
 | 
					    this.clients = {}
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,7 +27,9 @@ class SocketAuthority {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  getUsersOnline() {
 | 
					  getUsersOnline() {
 | 
				
			||||||
    const onlineUsersMap = {}
 | 
					    const onlineUsersMap = {}
 | 
				
			||||||
    Object.values(this.clients).filter(c => c.user).forEach(client => {
 | 
					    Object.values(this.clients)
 | 
				
			||||||
 | 
					      .filter((c) => c.user)
 | 
				
			||||||
 | 
					      .forEach((client) => {
 | 
				
			||||||
        if (onlineUsersMap[client.user.id]) {
 | 
					        if (onlineUsersMap[client.user.id]) {
 | 
				
			||||||
          onlineUsersMap[client.user.id].connections++
 | 
					          onlineUsersMap[client.user.id].connections++
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -32,7 +43,7 @@ class SocketAuthority {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getClientsForUser(userId) {
 | 
					  getClientsForUser(userId) {
 | 
				
			||||||
    return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
 | 
					    return Object.values(this.clients).filter((c) => c.user?.id === userId)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -67,7 +78,7 @@ class SocketAuthority {
 | 
				
			|||||||
  // Emits event to all admin user clients
 | 
					  // Emits event to all admin user clients
 | 
				
			||||||
  adminEmitter(evt, data) {
 | 
					  adminEmitter(evt, data) {
 | 
				
			||||||
    for (const socketId in this.clients) {
 | 
					    for (const socketId in this.clients) {
 | 
				
			||||||
      if (this.clients[socketId].user && this.clients[socketId].user.isAdminOrUp) {
 | 
					      if (this.clients[socketId].user?.isAdminOrUp) {
 | 
				
			||||||
        this.clients[socketId].socket.emit(evt, data)
 | 
					        this.clients[socketId].socket.emit(evt, data)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -81,10 +92,8 @@ class SocketAuthority {
 | 
				
			|||||||
  close(callback) {
 | 
					  close(callback) {
 | 
				
			||||||
    Logger.info('[SocketAuthority] Shutting down')
 | 
					    Logger.info('[SocketAuthority] Shutting down')
 | 
				
			||||||
    // This will close all open socket connections, and also close the underlying http server
 | 
					    // This will close all open socket connections, and also close the underlying http server
 | 
				
			||||||
    if (this.io)
 | 
					    if (this.io) this.io.close(callback)
 | 
				
			||||||
      this.io.close(callback)
 | 
					    else callback()
 | 
				
			||||||
    else
 | 
					 | 
				
			||||||
      callback()
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initialize(Server) {
 | 
					  initialize(Server) {
 | 
				
			||||||
@ -93,7 +102,7 @@ class SocketAuthority {
 | 
				
			|||||||
    this.io = new SocketIO.Server(this.Server.server, {
 | 
					    this.io = new SocketIO.Server(this.Server.server, {
 | 
				
			||||||
      cors: {
 | 
					      cors: {
 | 
				
			||||||
        origin: '*',
 | 
					        origin: '*',
 | 
				
			||||||
        methods: ["GET", "POST"]
 | 
					        methods: ['GET', 'POST']
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -144,7 +153,7 @@ class SocketAuthority {
 | 
				
			|||||||
        // admin user can send a message to all authenticated users
 | 
					        // admin user can send a message to all authenticated users
 | 
				
			||||||
        //   displays on the web app as a toast
 | 
					        //   displays on the web app as a toast
 | 
				
			||||||
        const client = this.clients[socket.id] || {}
 | 
					        const client = this.clients[socket.id] || {}
 | 
				
			||||||
        if (client.user && client.user.isAdminOrUp) {
 | 
					        if (client.user?.isAdminOrUp) {
 | 
				
			||||||
          this.emitter('admin_message', payload.message || '')
 | 
					          this.emitter('admin_message', payload.message || '')
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`)
 | 
					          Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`)
 | 
				
			||||||
@ -176,6 +185,7 @@ class SocketAuthority {
 | 
				
			|||||||
      Logger.error('Cannot validate socket - invalid token')
 | 
					      Logger.error('Cannot validate socket - invalid token')
 | 
				
			||||||
      return socket.emit('invalid_token')
 | 
					      return socket.emit('invalid_token')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // get the user via the id from the decoded jwt.
 | 
					    // get the user via the id from the decoded jwt.
 | 
				
			||||||
    const user = await Database.userModel.getUserByIdOrOldId(token_data.userId)
 | 
					    const user = await Database.userModel.getUserByIdOrOldId(token_data.userId)
 | 
				
			||||||
    if (!user) {
 | 
					    if (!user) {
 | 
				
			||||||
@ -196,18 +206,13 @@ class SocketAuthority {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    client.user = user
 | 
					    client.user = user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!client.user.toJSONForBrowser) {
 | 
					 | 
				
			||||||
      Logger.error('Invalid user...', client.user)
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Logger.debug(`[SocketAuthority] User Online ${client.user.username}`)
 | 
					    Logger.debug(`[SocketAuthority] User Online ${client.user.username}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
 | 
					    this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Update user lastSeen without firing sequelize bulk update hooks
 | 
					    // Update user lastSeen without firing sequelize bulk update hooks
 | 
				
			||||||
    user.lastSeen = Date.now()
 | 
					    user.lastSeen = Date.now()
 | 
				
			||||||
    await Database.userModel.updateFromOld(user, false)
 | 
					    await user.save({ hooks: false })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const initialPayload = {
 | 
					    const initialPayload = {
 | 
				
			||||||
      userId: client.user.id,
 | 
					      userId: client.user.id,
 | 
				
			||||||
 | 
				
			|||||||
@ -223,7 +223,7 @@ class LibraryController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // 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_updated', library.toJSON(), userFilter)
 | 
					      SocketAuthority.emitter('library_updated', library.toJSON(), userFilter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ class MiscController {
 | 
				
			|||||||
    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}`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const folder = library.folders.find(fold => fold.id === folderId)
 | 
					    const folder = library.folders.find((fold) => fold.id === folderId)
 | 
				
			||||||
    if (!folder) {
 | 
					    if (!folder) {
 | 
				
			||||||
      return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
 | 
					      return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -56,7 +56,7 @@ class MiscController {
 | 
				
			|||||||
    // `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`)
 | 
					    // `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`)
 | 
				
			||||||
    // before sanitizing all the directory parts to remove illegal chars and finally prepending
 | 
					    // before sanitizing all the directory parts to remove illegal chars and finally prepending
 | 
				
			||||||
    // the base folder path
 | 
					    // the base folder path
 | 
				
			||||||
    const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map(part => sanitizeFilename(part))
 | 
					    const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part))
 | 
				
			||||||
    const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts])
 | 
					    const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await fs.ensureDir(outputDirectory)
 | 
					    await fs.ensureDir(outputDirectory)
 | 
				
			||||||
@ -66,7 +66,8 @@ class MiscController {
 | 
				
			|||||||
    for (const file of files) {
 | 
					    for (const file of files) {
 | 
				
			||||||
      const path = Path.join(outputDirectory, sanitizeFilename(file.name))
 | 
					      const path = Path.join(outputDirectory, sanitizeFilename(file.name))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await file.mv(path)
 | 
					      await file
 | 
				
			||||||
 | 
					        .mv(path)
 | 
				
			||||||
        .then(() => {
 | 
					        .then(() => {
 | 
				
			||||||
          return true
 | 
					          return true
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@ -89,7 +90,7 @@ class MiscController {
 | 
				
			|||||||
    const includeArray = (req.query.include || '').split(',')
 | 
					    const includeArray = (req.query.include || '').split(',')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const data = {
 | 
					    const data = {
 | 
				
			||||||
      tasks: TaskManager.tasks.map(t => t.toJSON())
 | 
					      tasks: TaskManager.tasks.map((t) => t.toJSON())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (includeArray.includes('queue')) {
 | 
					    if (includeArray.includes('queue')) {
 | 
				
			||||||
@ -148,7 +149,7 @@ class MiscController {
 | 
				
			|||||||
    if (!sortingPrefixes?.length || !Array.isArray(sortingPrefixes)) {
 | 
					    if (!sortingPrefixes?.length || !Array.isArray(sortingPrefixes)) {
 | 
				
			||||||
      return res.status(400).send('Invalid request body')
 | 
					      return res.status(400).send('Invalid request body')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    sortingPrefixes = [...new Set(sortingPrefixes.map(p => p?.trim?.().toLowerCase()).filter(p => p))]
 | 
					    sortingPrefixes = [...new Set(sortingPrefixes.map((p) => p?.trim?.().toLowerCase()).filter((p) => p))]
 | 
				
			||||||
    if (!sortingPrefixes.length) {
 | 
					    if (!sortingPrefixes.length) {
 | 
				
			||||||
      return res.status(400).send('Invalid sortingPrefixes in request body')
 | 
					      return res.status(400).send('Invalid sortingPrefixes in request body')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -234,6 +235,8 @@ class MiscController {
 | 
				
			|||||||
   * POST: /api/authorize
 | 
					   * POST: /api/authorize
 | 
				
			||||||
   * Used to authorize an API token
 | 
					   * Used to authorize an API token
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
 | 
					   * @this import('../routers/ApiRouter')
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
   * @param {import('express').Request} req
 | 
					   * @param {import('express').Request} req
 | 
				
			||||||
   * @param {import('express').Response} res
 | 
					   * @param {import('express').Response} res
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@ -242,7 +245,7 @@ class MiscController {
 | 
				
			|||||||
      Logger.error('Invalid user in authorize')
 | 
					      Logger.error('Invalid user in authorize')
 | 
				
			||||||
      return res.sendStatus(401)
 | 
					      return res.sendStatus(401)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const userResponse = await this.auth.getUserLoginResponsePayload(req.user)
 | 
					    const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew)
 | 
				
			||||||
    res.json(userResponse)
 | 
					    res.json(userResponse)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -321,7 +324,7 @@ class MiscController {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (libraryItem.media.tags.includes(tag)) {
 | 
					      if (libraryItem.media.tags.includes(tag)) {
 | 
				
			||||||
        libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag) // Remove old tag
 | 
					        libraryItem.media.tags = libraryItem.media.tags.filter((t) => t !== tag) // Remove old tag
 | 
				
			||||||
        if (!libraryItem.media.tags.includes(newTag)) {
 | 
					        if (!libraryItem.media.tags.includes(newTag)) {
 | 
				
			||||||
          libraryItem.media.tags.push(newTag)
 | 
					          libraryItem.media.tags.push(newTag)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -367,7 +370,7 @@ class MiscController {
 | 
				
			|||||||
    // Remove tag from items
 | 
					    // Remove tag from items
 | 
				
			||||||
    for (const libraryItem of libraryItemsWithTag) {
 | 
					    for (const libraryItem of libraryItemsWithTag) {
 | 
				
			||||||
      Logger.debug(`[MiscController] Remove tag "${tag}" from item "${libraryItem.media.title}"`)
 | 
					      Logger.debug(`[MiscController] Remove tag "${tag}" from item "${libraryItem.media.title}"`)
 | 
				
			||||||
      libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag)
 | 
					      libraryItem.media.tags = libraryItem.media.tags.filter((t) => t !== tag)
 | 
				
			||||||
      await libraryItem.media.update({
 | 
					      await libraryItem.media.update({
 | 
				
			||||||
        tags: libraryItem.media.tags
 | 
					        tags: libraryItem.media.tags
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@ -456,7 +459,7 @@ class MiscController {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (libraryItem.media.genres.includes(genre)) {
 | 
					      if (libraryItem.media.genres.includes(genre)) {
 | 
				
			||||||
        libraryItem.media.genres = libraryItem.media.genres.filter(t => t !== genre) // Remove old genre
 | 
					        libraryItem.media.genres = libraryItem.media.genres.filter((t) => t !== genre) // Remove old genre
 | 
				
			||||||
        if (!libraryItem.media.genres.includes(newGenre)) {
 | 
					        if (!libraryItem.media.genres.includes(newGenre)) {
 | 
				
			||||||
          libraryItem.media.genres.push(newGenre)
 | 
					          libraryItem.media.genres.push(newGenre)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -502,7 +505,7 @@ class MiscController {
 | 
				
			|||||||
    // Remove genre from items
 | 
					    // Remove genre from items
 | 
				
			||||||
    for (const libraryItem of libraryItemsWithGenre) {
 | 
					    for (const libraryItem of libraryItemsWithGenre) {
 | 
				
			||||||
      Logger.debug(`[MiscController] Remove genre "${genre}" from item "${libraryItem.media.title}"`)
 | 
					      Logger.debug(`[MiscController] Remove genre "${genre}" from item "${libraryItem.media.title}"`)
 | 
				
			||||||
      libraryItem.media.genres = libraryItem.media.genres.filter(g => g !== genre)
 | 
					      libraryItem.media.genres = libraryItem.media.genres.filter((g) => g !== genre)
 | 
				
			||||||
      await libraryItem.media.update({
 | 
					      await libraryItem.media.update({
 | 
				
			||||||
        genres: libraryItem.media.genres
 | 
					        genres: libraryItem.media.genres
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@ -642,15 +645,13 @@ class MiscController {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const uris = settingsUpdate[key]
 | 
					        const uris = settingsUpdate[key]
 | 
				
			||||||
        if (!Array.isArray(uris) ||
 | 
					        if (!Array.isArray(uris) || (uris.includes('*') && uris.length > 1) || uris.some((uri) => uri !== '*' && !isValidRedirectURI(uri))) {
 | 
				
			||||||
          (uris.includes('*') && uris.length > 1) ||
 | 
					 | 
				
			||||||
          uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) {
 | 
					 | 
				
			||||||
          Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`)
 | 
					          Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`)
 | 
				
			||||||
          continue
 | 
					          continue
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Update the URIs
 | 
					        // Update the URIs
 | 
				
			||||||
        if (Database.serverSettings[key].some(uri => !uris.includes(uri)) || uris.some(uri => !Database.serverSettings[key].includes(uri))) {
 | 
					        if (Database.serverSettings[key].some((uri) => !uris.includes(uri)) || uris.some((uri) => !Database.serverSettings[key].includes(uri))) {
 | 
				
			||||||
          Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`)
 | 
					          Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`)
 | 
				
			||||||
          Database.serverSettings[key] = uris
 | 
					          Database.serverSettings[key] = uris
 | 
				
			||||||
          hasUpdates = true
 | 
					          hasUpdates = true
 | 
				
			||||||
 | 
				
			|||||||
@ -31,8 +31,8 @@ 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 allUsers = await Database.userModel.getOldUsers()
 | 
					    const allUsers = await Database.userModel.findAll()
 | 
				
			||||||
    const users = allUsers.map((u) => u.toJSONForBrowser(hideRootToken, true))
 | 
					    const users = allUsers.map((u) => u.toOldJSONForBrowser(hideRootToken, true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (includes.includes('latestSession')) {
 | 
					    if (includes.includes('latestSession')) {
 | 
				
			||||||
      for (const user of users) {
 | 
					      for (const user of users) {
 | 
				
			||||||
@ -106,7 +106,7 @@ class UserController {
 | 
				
			|||||||
    const account = req.body
 | 
					    const account = req.body
 | 
				
			||||||
    const username = account.username
 | 
					    const username = account.username
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const usernameExists = await Database.userModel.getUserByUsername(username)
 | 
					    const usernameExists = await Database.userModel.checkUserExistsWithUsername(username)
 | 
				
			||||||
    if (usernameExists) {
 | 
					    if (usernameExists) {
 | 
				
			||||||
      return res.status(500).send('Username already taken')
 | 
					      return res.status(500).send('Username already taken')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -149,7 +149,7 @@ class UserController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // When changing username create a new API token
 | 
					    // When changing username create a new API token
 | 
				
			||||||
    if (account.username !== undefined && account.username !== user.username) {
 | 
					    if (account.username !== undefined && account.username !== user.username) {
 | 
				
			||||||
      const usernameExists = await Database.userModel.getUserByUsername(account.username)
 | 
					      const usernameExists = await Database.userModel.checkUserExistsWithUsername(account.username)
 | 
				
			||||||
      if (usernameExists) {
 | 
					      if (usernameExists) {
 | 
				
			||||||
        return res.status(500).send('Username already taken')
 | 
					        return res.status(500).send('Username already taken')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -272,7 +272,8 @@ class UserController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.params.id) {
 | 
					    if (req.params.id) {
 | 
				
			||||||
      req.reqUser = await Database.userModel.getUserById(req.params.id)
 | 
					      // TODO: Update to use new user model
 | 
				
			||||||
 | 
					      req.reqUser = await Database.userModel.getOldUserById(req.params.id)
 | 
				
			||||||
      if (!req.reqUser) {
 | 
					      if (!req.reqUser) {
 | 
				
			||||||
        return res.sendStatus(404)
 | 
					        return res.sendStatus(404)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
				
			|||||||
@ -34,29 +34,6 @@ class MediaProgress extends Model {
 | 
				
			|||||||
    this.createdAt
 | 
					    this.createdAt
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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 || 0,
 | 
					 | 
				
			||||||
      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) {
 | 
					  static upsertFromOld(oldMediaProgress) {
 | 
				
			||||||
    const mediaProgress = this.getFromOld(oldMediaProgress)
 | 
					    const mediaProgress = this.getFromOld(oldMediaProgress)
 | 
				
			||||||
    return this.upsert(mediaProgress)
 | 
					    return this.upsert(mediaProgress)
 | 
				
			||||||
@ -182,6 +159,29 @@ class MediaProgress extends Model {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
    MediaProgress.belongsTo(user)
 | 
					    MediaProgress.belongsTo(user)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 || 0,
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = MediaProgress
 | 
					module.exports = MediaProgress
 | 
				
			||||||
 | 
				
			|||||||
@ -42,31 +42,41 @@ class User extends Model {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get all oldUsers
 | 
					   *
 | 
				
			||||||
   * @returns {Promise<oldUser>}
 | 
					   * @param {string} type
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async getOldUsers() {
 | 
					  static getDefaultPermissionsForUserType(type) {
 | 
				
			||||||
    const users = await this.findAll({
 | 
					    return {
 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					      download: true,
 | 
				
			||||||
    })
 | 
					      update: type === 'root' || type === 'admin',
 | 
				
			||||||
    return users.map((u) => this.getOldUser(u))
 | 
					      delete: type === 'root',
 | 
				
			||||||
 | 
					      upload: type === 'root' || type === 'admin',
 | 
				
			||||||
 | 
					      accessAllLibraries: true,
 | 
				
			||||||
 | 
					      accessAllTags: true,
 | 
				
			||||||
 | 
					      accessExplicitContent: true,
 | 
				
			||||||
 | 
					      librariesAccessible: [],
 | 
				
			||||||
 | 
					      itemTagsSelected: []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get old user model from new
 | 
					   * Get old user model from new
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {Object} userExpanded
 | 
					   * @param {User} userExpanded
 | 
				
			||||||
   * @returns {oldUser}
 | 
					   * @returns {oldUser}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static getOldUser(userExpanded) {
 | 
					  static getOldUser(userExpanded) {
 | 
				
			||||||
    const mediaProgress = userExpanded.mediaProgresses.map((mp) => mp.getOldMediaProgress())
 | 
					    const mediaProgress = userExpanded.mediaProgresses.map((mp) => mp.getOldMediaProgress())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const librariesAccessible = userExpanded.permissions?.librariesAccessible || []
 | 
					    const librariesAccessible = [...(userExpanded.permissions?.librariesAccessible || [])]
 | 
				
			||||||
    const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || []
 | 
					    const itemTagsSelected = [...(userExpanded.permissions?.itemTagsSelected || [])]
 | 
				
			||||||
    const permissions = userExpanded.permissions || {}
 | 
					    const permissions = { ...(userExpanded.permissions || {}) }
 | 
				
			||||||
    delete permissions.librariesAccessible
 | 
					    delete permissions.librariesAccessible
 | 
				
			||||||
    delete permissions.itemTagsSelected
 | 
					    delete permissions.itemTagsSelected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const seriesHideFromContinueListening = userExpanded.extraData?.seriesHideFromContinueListening || []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new oldUser({
 | 
					    return new oldUser({
 | 
				
			||||||
      id: userExpanded.id,
 | 
					      id: userExpanded.id,
 | 
				
			||||||
      oldUserId: userExpanded.extraData?.oldUserId || null,
 | 
					      oldUserId: userExpanded.extraData?.oldUserId || null,
 | 
				
			||||||
@ -76,7 +86,7 @@ class User extends Model {
 | 
				
			|||||||
      type: userExpanded.type,
 | 
					      type: userExpanded.type,
 | 
				
			||||||
      token: userExpanded.token,
 | 
					      token: userExpanded.token,
 | 
				
			||||||
      mediaProgress,
 | 
					      mediaProgress,
 | 
				
			||||||
      seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [],
 | 
					      seriesHideFromContinueListening: [...seriesHideFromContinueListening],
 | 
				
			||||||
      bookmarks: userExpanded.bookmarks,
 | 
					      bookmarks: userExpanded.bookmarks,
 | 
				
			||||||
      isActive: userExpanded.isActive,
 | 
					      isActive: userExpanded.isActive,
 | 
				
			||||||
      isLocked: userExpanded.isLocked,
 | 
					      isLocked: userExpanded.isLocked,
 | 
				
			||||||
@ -168,32 +178,35 @@ class User extends Model {
 | 
				
			|||||||
   * Create root user
 | 
					   * Create root user
 | 
				
			||||||
   * @param {string} username
 | 
					   * @param {string} username
 | 
				
			||||||
   * @param {string} pash
 | 
					   * @param {string} pash
 | 
				
			||||||
   * @param {Auth} auth
 | 
					   * @param {import('../Auth')} auth
 | 
				
			||||||
   * @returns {Promise<oldUser>}
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async createRootUser(username, pash, auth) {
 | 
					  static async createRootUser(username, pash, auth) {
 | 
				
			||||||
    const userId = uuidv4()
 | 
					    const userId = uuidv4()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const token = await auth.generateAccessToken({ id: userId, username })
 | 
					    const token = await auth.generateAccessToken({ id: userId, username })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newRoot = new oldUser({
 | 
					    const newUser = {
 | 
				
			||||||
      id: userId,
 | 
					      id: userId,
 | 
				
			||||||
      type: 'root',
 | 
					      type: 'root',
 | 
				
			||||||
      username,
 | 
					      username,
 | 
				
			||||||
      pash,
 | 
					      pash,
 | 
				
			||||||
      token,
 | 
					      token,
 | 
				
			||||||
      isActive: true,
 | 
					      isActive: true,
 | 
				
			||||||
      createdAt: Date.now()
 | 
					      permissions: this.getDefaultPermissionsForUserType('root'),
 | 
				
			||||||
    })
 | 
					      bookmarks: [],
 | 
				
			||||||
    await this.createFromOld(newRoot)
 | 
					      extraData: {
 | 
				
			||||||
    return newRoot
 | 
					        seriesHideFromContinueListening: []
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this.create(newUser)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Create user from openid userinfo
 | 
					   * Create user from openid userinfo
 | 
				
			||||||
   * @param {Object} userinfo
 | 
					   * @param {Object} userinfo
 | 
				
			||||||
   * @param {Auth} auth
 | 
					   * @param {import('../Auth')} auth
 | 
				
			||||||
   * @returns {Promise<oldUser>}
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async createUserFromOpenIdUserInfo(userinfo, auth) {
 | 
					  static async createUserFromOpenIdUserInfo(userinfo, auth) {
 | 
				
			||||||
    const userId = uuidv4()
 | 
					    const userId = uuidv4()
 | 
				
			||||||
@ -203,7 +216,7 @@ class User extends Model {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const token = await auth.generateAccessToken({ id: userId, username })
 | 
					    const token = await auth.generateAccessToken({ id: userId, username })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newUser = new oldUser({
 | 
					    const newUser = {
 | 
				
			||||||
      id: userId,
 | 
					      id: userId,
 | 
				
			||||||
      type: 'user',
 | 
					      type: 'user',
 | 
				
			||||||
      username,
 | 
					      username,
 | 
				
			||||||
@ -211,51 +224,30 @@ class User extends Model {
 | 
				
			|||||||
      pash: null,
 | 
					      pash: null,
 | 
				
			||||||
      token,
 | 
					      token,
 | 
				
			||||||
      isActive: true,
 | 
					      isActive: true,
 | 
				
			||||||
 | 
					      permissions: this.getDefaultPermissionsForUserType('user'),
 | 
				
			||||||
 | 
					      bookmarks: [],
 | 
				
			||||||
 | 
					      extraData: {
 | 
				
			||||||
        authOpenIDSub: userinfo.sub,
 | 
					        authOpenIDSub: userinfo.sub,
 | 
				
			||||||
      createdAt: Date.now()
 | 
					        seriesHideFromContinueListening: []
 | 
				
			||||||
    })
 | 
					      }
 | 
				
			||||||
    if (await this.createFromOld(newUser)) {
 | 
					    }
 | 
				
			||||||
      SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
 | 
					    const user = await this.create(newUser)
 | 
				
			||||||
      return newUser
 | 
					
 | 
				
			||||||
 | 
					    if (user) {
 | 
				
			||||||
 | 
					      SocketAuthority.adminEmitter('user_added', user.toOldJSONForBrowser())
 | 
				
			||||||
 | 
					      return user
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Get a user by id or by the old database id
 | 
					 | 
				
			||||||
   * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id
 | 
					 | 
				
			||||||
   * @param {string} userId
 | 
					 | 
				
			||||||
   * @returns {Promise<oldUser|null>} null if not found
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  static async getUserByIdOrOldId(userId) {
 | 
					 | 
				
			||||||
    if (!userId) return null
 | 
					 | 
				
			||||||
    const user = await this.findOne({
 | 
					 | 
				
			||||||
      where: {
 | 
					 | 
				
			||||||
        [sequelize.Op.or]: [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            id: userId
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            extraData: {
 | 
					 | 
				
			||||||
              [sequelize.Op.substring]: userId
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    if (!user) return null
 | 
					 | 
				
			||||||
    return this.getOldUser(user)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get user by username case insensitive
 | 
					   * Get user by username case insensitive
 | 
				
			||||||
   * @param {string} username
 | 
					   * @param {string} username
 | 
				
			||||||
   * @returns {Promise<oldUser|null>} returns null if not found
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async getUserByUsername(username) {
 | 
					  static async getUserByUsername(username) {
 | 
				
			||||||
    if (!username) return null
 | 
					    if (!username) return null
 | 
				
			||||||
    const user = await this.findOne({
 | 
					    return this.findOne({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        username: {
 | 
					        username: {
 | 
				
			||||||
          [sequelize.Op.like]: username
 | 
					          [sequelize.Op.like]: username
 | 
				
			||||||
@ -263,18 +255,16 @@ class User extends Model {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					      include: this.sequelize.models.mediaProgress
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if (!user) return null
 | 
					 | 
				
			||||||
    return this.getOldUser(user)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get user by email case insensitive
 | 
					   * Get user by email case insensitive
 | 
				
			||||||
   * @param {string} username
 | 
					   * @param {string} email
 | 
				
			||||||
   * @returns {Promise<oldUser|null>} returns null if not found
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async getUserByEmail(email) {
 | 
					  static async getUserByEmail(email) {
 | 
				
			||||||
    if (!email) return null
 | 
					    if (!email) return null
 | 
				
			||||||
    const user = await this.findOne({
 | 
					    return this.findOne({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        email: {
 | 
					        email: {
 | 
				
			||||||
          [sequelize.Op.like]: email
 | 
					          [sequelize.Op.like]: email
 | 
				
			||||||
@ -282,20 +272,45 @@ class User extends Model {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					      include: this.sequelize.models.mediaProgress
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if (!user) return null
 | 
					 | 
				
			||||||
    return this.getOldUser(user)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get user by id
 | 
					   * Get user by id
 | 
				
			||||||
   * @param {string} userId
 | 
					   * @param {string} userId
 | 
				
			||||||
   * @returns {Promise<oldUser|null>} returns null if not found
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async getUserById(userId) {
 | 
					  static async getUserById(userId) {
 | 
				
			||||||
    if (!userId) return null
 | 
					    if (!userId) return null
 | 
				
			||||||
    const user = await this.findByPk(userId, {
 | 
					    return this.findByPk(userId, {
 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					      include: this.sequelize.models.mediaProgress
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get user by id or old id
 | 
				
			||||||
 | 
					   * JWT tokens generated before 2.3.0 used old user ids
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string} userId
 | 
				
			||||||
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async getUserByIdOrOldId(userId) {
 | 
				
			||||||
 | 
					    if (!userId) return null
 | 
				
			||||||
 | 
					    return this.findOne({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        [sequelize.Op.or]: [{ id: userId }, { 'extraData.oldUserId': userId }]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      include: this.sequelize.models.mediaProgress
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @deprecated
 | 
				
			||||||
 | 
					   * Get old user by id
 | 
				
			||||||
 | 
					   * @param {string} userId
 | 
				
			||||||
 | 
					   * @returns {Promise<oldUser|null>} returns null if not found
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async getOldUserById(userId) {
 | 
				
			||||||
 | 
					    const user = await this.getUserById(userId)
 | 
				
			||||||
    if (!user) return null
 | 
					    if (!user) return null
 | 
				
			||||||
    return this.getOldUser(user)
 | 
					    return this.getOldUser(user)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -303,16 +318,14 @@ class User extends Model {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get user by openid sub
 | 
					   * Get user by openid sub
 | 
				
			||||||
   * @param {string} sub
 | 
					   * @param {string} sub
 | 
				
			||||||
   * @returns {Promise<oldUser|null>} returns null if not found
 | 
					   * @returns {Promise<User>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  static async getUserByOpenIDSub(sub) {
 | 
					  static async getUserByOpenIDSub(sub) {
 | 
				
			||||||
    if (!sub) return null
 | 
					    if (!sub) return null
 | 
				
			||||||
    const user = await this.findOne({
 | 
					    return this.findOne({
 | 
				
			||||||
      where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
 | 
					      where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
 | 
				
			||||||
      include: this.sequelize.models.mediaProgress
 | 
					      include: this.sequelize.models.mediaProgress
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    if (!user) return null
 | 
					 | 
				
			||||||
    return this.getOldUser(user)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -344,6 +357,20 @@ class User extends Model {
 | 
				
			|||||||
    return count > 0
 | 
					    return count > 0
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Check if user exists with username
 | 
				
			||||||
 | 
					   * @param {string} username
 | 
				
			||||||
 | 
					   * @returns {boolean}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async checkUserExistsWithUsername(username) {
 | 
				
			||||||
 | 
					    const count = await this.count({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        username
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return count > 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Initialize model
 | 
					   * Initialize model
 | 
				
			||||||
   * @param {import('../Database').sequelize} sequelize
 | 
					   * @param {import('../Database').sequelize} sequelize
 | 
				
			||||||
@ -380,6 +407,99 @@ class User extends Model {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get isAdminOrUp() {
 | 
				
			||||||
 | 
					    return this.type === 'root' || this.type === 'admin'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get isUser() {
 | 
				
			||||||
 | 
					    return this.type === 'user'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  /** @type {string|null} */
 | 
				
			||||||
 | 
					  get authOpenIDSub() {
 | 
				
			||||||
 | 
					    return this.extraData?.authOpenIDSub || null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * User data for clients
 | 
				
			||||||
 | 
					   * Emitted on socket events user_online, user_offline and user_stream_update
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {import('../objects/PlaybackSession')[]} sessions
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  toJSONForPublic(sessions) {
 | 
				
			||||||
 | 
					    const session = sessions?.find((s) => s.userId === this.id)?.toJSONForClient() || null
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      username: this.username,
 | 
				
			||||||
 | 
					      type: this.type,
 | 
				
			||||||
 | 
					      session,
 | 
				
			||||||
 | 
					      lastSeen: this.lastSeen?.valueOf() || null,
 | 
				
			||||||
 | 
					      createdAt: this.createdAt.valueOf()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * User data for browser using old model
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {boolean} [hideRootToken=false]
 | 
				
			||||||
 | 
					   * @param {boolean} [minimal=false]
 | 
				
			||||||
 | 
					   * @returns
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  toOldJSONForBrowser(hideRootToken = false, minimal = false) {
 | 
				
			||||||
 | 
					    const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || []
 | 
				
			||||||
 | 
					    const librariesAccessible = this.permissions?.librariesAccessible || []
 | 
				
			||||||
 | 
					    const itemTagsSelected = this.permissions?.itemTagsSelected || []
 | 
				
			||||||
 | 
					    const permissions = { ...this.permissions }
 | 
				
			||||||
 | 
					    delete permissions.librariesAccessible
 | 
				
			||||||
 | 
					    delete permissions.itemTagsSelected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const json = {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      username: this.username,
 | 
				
			||||||
 | 
					      email: this.email,
 | 
				
			||||||
 | 
					      type: this.type,
 | 
				
			||||||
 | 
					      token: this.type === 'root' && hideRootToken ? '' : this.token,
 | 
				
			||||||
 | 
					      mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
 | 
				
			||||||
 | 
					      seriesHideFromContinueListening: [...seriesHideFromContinueListening],
 | 
				
			||||||
 | 
					      bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],
 | 
				
			||||||
 | 
					      isActive: this.isActive,
 | 
				
			||||||
 | 
					      isLocked: this.isLocked,
 | 
				
			||||||
 | 
					      lastSeen: this.lastSeen?.valueOf() || null,
 | 
				
			||||||
 | 
					      createdAt: this.createdAt.valueOf(),
 | 
				
			||||||
 | 
					      permissions: permissions,
 | 
				
			||||||
 | 
					      librariesAccessible: [...librariesAccessible],
 | 
				
			||||||
 | 
					      itemTagsSelected: [...itemTagsSelected],
 | 
				
			||||||
 | 
					      hasOpenIDLink: !!this.authOpenIDSub
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (minimal) {
 | 
				
			||||||
 | 
					      delete json.mediaProgress
 | 
				
			||||||
 | 
					      delete json.bookmarks
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return json
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Check user has access to library
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string} libraryId
 | 
				
			||||||
 | 
					   * @returns {boolean}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  checkCanAccessLibrary(libraryId) {
 | 
				
			||||||
 | 
					    if (this.permissions?.accessAllLibraries) return true
 | 
				
			||||||
 | 
					    if (!this.permissions?.librariesAccessible) return false
 | 
				
			||||||
 | 
					    return this.permissions.librariesAccessible.includes(libraryId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get first available library id for user
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {string[]} libraryIds
 | 
				
			||||||
 | 
					   * @returns {string|null}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getDefaultLibraryId(libraryIds) {
 | 
				
			||||||
 | 
					    // Libraries should already be in ascending display order, find first accessible
 | 
				
			||||||
 | 
					    return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = User
 | 
					module.exports = User
 | 
				
			||||||
 | 
				
			|||||||
@ -140,7 +140,7 @@ class EmailSettings {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {EreaderDeviceObject} device
 | 
					   * @param {EreaderDeviceObject} device
 | 
				
			||||||
   * @param {import('../user/User')} user
 | 
					   * @param {import('../../models/User')} user
 | 
				
			||||||
   * @returns {boolean}
 | 
					   * @returns {boolean}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  checkUserCanAccessDevice(device, user) {
 | 
					  checkUserCanAccessDevice(device, user) {
 | 
				
			||||||
@ -158,7 +158,7 @@ class EmailSettings {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get ereader devices accessible to user
 | 
					   * Get ereader devices accessible to user
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param {import('../user/User')} user
 | 
					   * @param {import('../../models/User')} user
 | 
				
			||||||
   * @returns {EreaderDeviceObject[]}
 | 
					   * @returns {EreaderDeviceObject[]}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  getEReaderDevices(user) {
 | 
					  getEReaderDevices(user) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
const { DataTypes, QueryInterface } = require('sequelize')
 | 
					const { DataTypes, QueryInterface } = require('sequelize')
 | 
				
			||||||
const Path = require('path')
 | 
					const Path = require('path')
 | 
				
			||||||
const uuidv4 = require("uuid").v4
 | 
					const uuidv4 = require('uuid').v4
 | 
				
			||||||
const Logger = require('../../Logger')
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
const fs = require('../../libs/fsExtra')
 | 
					const fs = require('../../libs/fsExtra')
 | 
				
			||||||
const oldDbFiles = require('./oldDbFiles')
 | 
					const oldDbFiles = require('./oldDbFiles')
 | 
				
			||||||
@ -36,18 +36,7 @@ function getDeviceInfoString(deviceInfo, UserId) {
 | 
				
			|||||||
  if (!deviceInfo) return null
 | 
					  if (!deviceInfo) return null
 | 
				
			||||||
  if (deviceInfo.deviceId) return deviceInfo.deviceId
 | 
					  if (deviceInfo.deviceId) return deviceInfo.deviceId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const keys = [
 | 
					  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 || '')
 | 
				
			||||||
    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')
 | 
					  return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,7 +56,7 @@ function migrateBook(oldLibraryItem, LibraryItem) {
 | 
				
			|||||||
    bookAuthor: []
 | 
					    bookAuthor: []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const tracks = (oldBook.audioFiles || []).filter(af => !af.exclude && !af.invalid)
 | 
					  const tracks = (oldBook.audioFiles || []).filter((af) => !af.exclude && !af.invalid)
 | 
				
			||||||
  let duration = 0
 | 
					  let duration = 0
 | 
				
			||||||
  for (const track of tracks) {
 | 
					  for (const track of tracks) {
 | 
				
			||||||
    if (track.duration !== null && !isNaN(track.duration)) {
 | 
					    if (track.duration !== null && !isNaN(track.duration)) {
 | 
				
			||||||
@ -298,7 +287,7 @@ function migrateLibraryItems(oldLibraryItems) {
 | 
				
			|||||||
      updatedAt: oldLibraryItem.updatedAt,
 | 
					      updatedAt: oldLibraryItem.updatedAt,
 | 
				
			||||||
      libraryId,
 | 
					      libraryId,
 | 
				
			||||||
      libraryFolderId,
 | 
					      libraryFolderId,
 | 
				
			||||||
      libraryFiles: oldLibraryItem.libraryFiles.map(lf => {
 | 
					      libraryFiles: oldLibraryItem.libraryFiles.map((lf) => {
 | 
				
			||||||
        if (lf.isSupplementary === undefined) lf.isSupplementary = null
 | 
					        if (lf.isSupplementary === undefined) lf.isSupplementary = null
 | 
				
			||||||
        return lf
 | 
					        return lf
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@ -390,13 +379,19 @@ function migrateAuthors(oldAuthors, oldLibraryItems) {
 | 
				
			|||||||
  const _newRecords = []
 | 
					  const _newRecords = []
 | 
				
			||||||
  for (const oldAuthor of oldAuthors) {
 | 
					  for (const oldAuthor of oldAuthors) {
 | 
				
			||||||
    // Get an array of NEW library ids that have this author
 | 
					    // Get an array of NEW library ids that have this author
 | 
				
			||||||
    const librariesWithThisAuthor = [...new Set(oldLibraryItems.map(li => {
 | 
					    const librariesWithThisAuthor = [
 | 
				
			||||||
      if (!li.media.metadata.authors?.some(au => au.id === oldAuthor.id)) return null
 | 
					      ...new Set(
 | 
				
			||||||
 | 
					        oldLibraryItems
 | 
				
			||||||
 | 
					          .map((li) => {
 | 
				
			||||||
 | 
					            if (!li.media.metadata.authors?.some((au) => au.id === oldAuthor.id)) return null
 | 
				
			||||||
            if (!oldDbIdMap.libraries[li.libraryId]) {
 | 
					            if (!oldDbIdMap.libraries[li.libraryId]) {
 | 
				
			||||||
              Logger.warn(`[dbMigration] Authors library id ${li.libraryId} was not migrated`)
 | 
					              Logger.warn(`[dbMigration] Authors library id ${li.libraryId} was not migrated`)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return oldDbIdMap.libraries[li.libraryId]
 | 
					            return oldDbIdMap.libraries[li.libraryId]
 | 
				
			||||||
    }).filter(lid => lid))]
 | 
					          })
 | 
				
			||||||
 | 
					          .filter((lid) => lid)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!librariesWithThisAuthor.length) {
 | 
					    if (!librariesWithThisAuthor.length) {
 | 
				
			||||||
      Logger.error(`[dbMigration] Author ${oldAuthor.name} was not found in any libraries`)
 | 
					      Logger.error(`[dbMigration] Author ${oldAuthor.name} was not found in any libraries`)
 | 
				
			||||||
@ -436,10 +431,16 @@ function migrateSeries(oldSerieses, oldLibraryItems) {
 | 
				
			|||||||
  // Series will be separate between libraries
 | 
					  // Series will be separate between libraries
 | 
				
			||||||
  for (const oldSeries of oldSerieses) {
 | 
					  for (const oldSeries of oldSerieses) {
 | 
				
			||||||
    // Get an array of NEW library ids that have this series
 | 
					    // Get an array of NEW library ids that have this series
 | 
				
			||||||
    const librariesWithThisSeries = [...new Set(oldLibraryItems.map(li => {
 | 
					    const librariesWithThisSeries = [
 | 
				
			||||||
      if (!li.media.metadata.series?.some(se => se.id === oldSeries.id)) return null
 | 
					      ...new Set(
 | 
				
			||||||
 | 
					        oldLibraryItems
 | 
				
			||||||
 | 
					          .map((li) => {
 | 
				
			||||||
 | 
					            if (!li.media.metadata.series?.some((se) => se.id === oldSeries.id)) return null
 | 
				
			||||||
            return oldDbIdMap.libraries[li.libraryId]
 | 
					            return oldDbIdMap.libraries[li.libraryId]
 | 
				
			||||||
    }).filter(lid => lid))]
 | 
					          })
 | 
				
			||||||
 | 
					          .filter((lid) => lid)
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!librariesWithThisSeries.length) {
 | 
					    if (!librariesWithThisSeries.length) {
 | 
				
			||||||
      Logger.error(`[dbMigration] Series ${oldSeries.name} was not found in any libraries`)
 | 
					      Logger.error(`[dbMigration] Series ${oldSeries.name} was not found in any libraries`)
 | 
				
			||||||
@ -478,16 +479,19 @@ function migrateUsers(oldUsers) {
 | 
				
			|||||||
    // Migrate User
 | 
					    // Migrate User
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    // Convert old library ids to new ids
 | 
					    // Convert old library ids to new ids
 | 
				
			||||||
    const librariesAccessible = (oldUser.librariesAccessible || []).map((lid) => oldDbIdMap.libraries[lid]).filter(li => li)
 | 
					    const librariesAccessible = (oldUser.librariesAccessible || []).map((lid) => oldDbIdMap.libraries[lid]).filter((li) => li)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Convert old library item ids to new ids
 | 
					    // Convert old library item ids to new ids
 | 
				
			||||||
    const bookmarks = (oldUser.bookmarks || []).map(bm => {
 | 
					    const bookmarks = (oldUser.bookmarks || [])
 | 
				
			||||||
 | 
					      .map((bm) => {
 | 
				
			||||||
        bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
 | 
					        bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
 | 
				
			||||||
        return bm
 | 
					        return bm
 | 
				
			||||||
    }).filter(bm => bm.libraryItemId)
 | 
					      })
 | 
				
			||||||
 | 
					      .filter((bm) => bm.libraryItemId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Convert old series ids to new
 | 
					    // Convert old series ids to new
 | 
				
			||||||
    const seriesHideFromContinueListening = (oldUser.seriesHideFromContinueListening || []).map(oldSeriesId => {
 | 
					    const seriesHideFromContinueListening = (oldUser.seriesHideFromContinueListening || [])
 | 
				
			||||||
 | 
					      .map((oldSeriesId) => {
 | 
				
			||||||
        // Series were split to be per library
 | 
					        // Series were split to be per library
 | 
				
			||||||
        // This will use the first series it finds
 | 
					        // This will use the first series it finds
 | 
				
			||||||
        for (const libraryId in oldDbIdMap.series) {
 | 
					        for (const libraryId in oldDbIdMap.series) {
 | 
				
			||||||
@ -496,7 +500,8 @@ function migrateUsers(oldUsers) {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return null
 | 
					        return null
 | 
				
			||||||
    }).filter(se => se)
 | 
					      })
 | 
				
			||||||
 | 
					      .filter((se) => se)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const User = {
 | 
					    const User = {
 | 
				
			||||||
      id: uuidv4(),
 | 
					      id: uuidv4(),
 | 
				
			||||||
@ -705,7 +710,7 @@ function migrateCollections(oldCollections) {
 | 
				
			|||||||
      continue
 | 
					      continue
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid)
 | 
					    const BookIds = oldCollection.books.map((lid) => oldDbIdMap.books[lid]).filter((bid) => bid)
 | 
				
			||||||
    if (!BookIds.length) {
 | 
					    if (!BookIds.length) {
 | 
				
			||||||
      Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`)
 | 
					      Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`)
 | 
				
			||||||
      continue
 | 
					      continue
 | 
				
			||||||
@ -912,9 +917,9 @@ function migrateFeeds(oldFeeds) {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
function migrateSettings(oldSettings) {
 | 
					function migrateSettings(oldSettings) {
 | 
				
			||||||
  const _newRecords = []
 | 
					  const _newRecords = []
 | 
				
			||||||
  const serverSettings = oldSettings.find(s => s.id === 'server-settings')
 | 
					  const serverSettings = oldSettings.find((s) => s.id === 'server-settings')
 | 
				
			||||||
  const notificationSettings = oldSettings.find(s => s.id === 'notification-settings')
 | 
					  const notificationSettings = oldSettings.find((s) => s.id === 'notification-settings')
 | 
				
			||||||
  const emailSettings = oldSettings.find(s => s.id === 'email-settings')
 | 
					  const emailSettings = oldSettings.find((s) => s.id === 'email-settings')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (serverSettings) {
 | 
					  if (serverSettings) {
 | 
				
			||||||
    _newRecords.push({
 | 
					    _newRecords.push({
 | 
				
			||||||
@ -1055,7 +1060,6 @@ async function handleMigrateSessions(DatabaseModels) {
 | 
				
			|||||||
      await DatabaseModels[model].bulkCreate(newSessionRecords[model])
 | 
					      await DatabaseModels[model].bulkCreate(newSessionRecords[model])
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -1156,17 +1160,32 @@ module.exports.checkShouldMigrate = async () => {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
async function migrationPatchNewColumns(queryInterface) {
 | 
					async function migrationPatchNewColumns(queryInterface) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return queryInterface.sequelize.transaction(t => {
 | 
					    return queryInterface.sequelize.transaction((t) => {
 | 
				
			||||||
      return Promise.all([
 | 
					      return Promise.all([
 | 
				
			||||||
        queryInterface.addColumn('libraryItems', 'extraData', {
 | 
					        queryInterface.addColumn(
 | 
				
			||||||
 | 
					          'libraryItems',
 | 
				
			||||||
 | 
					          'extraData',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
            type: DataTypes.JSON
 | 
					            type: DataTypes.JSON
 | 
				
			||||||
        }, { transaction: t }),
 | 
					          },
 | 
				
			||||||
        queryInterface.addColumn('podcastEpisodes', 'extraData', {
 | 
					          { transaction: t }
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        queryInterface.addColumn(
 | 
				
			||||||
 | 
					          'podcastEpisodes',
 | 
				
			||||||
 | 
					          'extraData',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
            type: DataTypes.JSON
 | 
					            type: DataTypes.JSON
 | 
				
			||||||
        }, { transaction: t }),
 | 
					          },
 | 
				
			||||||
        queryInterface.addColumn('libraries', 'extraData', {
 | 
					          { transaction: t }
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        queryInterface.addColumn(
 | 
				
			||||||
 | 
					          'libraries',
 | 
				
			||||||
 | 
					          'extraData',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
            type: DataTypes.JSON
 | 
					            type: DataTypes.JSON
 | 
				
			||||||
        }, { transaction: t })
 | 
					          },
 | 
				
			||||||
 | 
					          { transaction: t }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      ])
 | 
					      ])
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
@ -1188,7 +1207,7 @@ async function handleOldLibraryItems(ctx) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  for (const libraryItem of libraryItems) {
 | 
					  for (const libraryItem of libraryItems) {
 | 
				
			||||||
    // Find matching old library item by ino
 | 
					    // Find matching old library item by ino
 | 
				
			||||||
    const matchingOldLibraryItem = oldLibraryItems.find(oli => oli.ino === libraryItem.ino)
 | 
					    const matchingOldLibraryItem = oldLibraryItems.find((oli) => oli.ino === libraryItem.ino)
 | 
				
			||||||
    if (matchingOldLibraryItem) {
 | 
					    if (matchingOldLibraryItem) {
 | 
				
			||||||
      oldDbIdMap.libraryItems[matchingOldLibraryItem.id] = libraryItem.id
 | 
					      oldDbIdMap.libraryItems[matchingOldLibraryItem.id] = libraryItem.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1202,7 +1221,7 @@ async function handleOldLibraryItems(ctx) {
 | 
				
			|||||||
      if (libraryItem.media.episodes?.length && matchingOldLibraryItem.media.episodes?.length) {
 | 
					      if (libraryItem.media.episodes?.length && matchingOldLibraryItem.media.episodes?.length) {
 | 
				
			||||||
        for (const podcastEpisode of libraryItem.media.episodes) {
 | 
					        for (const podcastEpisode of libraryItem.media.episodes) {
 | 
				
			||||||
          // Find matching old episode by audio file ino
 | 
					          // Find matching old episode by audio file ino
 | 
				
			||||||
          const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find(oep => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino)
 | 
					          const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find((oep) => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino)
 | 
				
			||||||
          if (matchingOldPodcastEpisode) {
 | 
					          if (matchingOldPodcastEpisode) {
 | 
				
			||||||
            oldDbIdMap.podcastEpisodes[matchingOldPodcastEpisode.id] = podcastEpisode.id
 | 
					            oldDbIdMap.podcastEpisodes[matchingOldPodcastEpisode.id] = podcastEpisode.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1244,11 +1263,11 @@ async function handleOldLibraries(ctx) {
 | 
				
			|||||||
  let librariesUpdated = 0
 | 
					  let librariesUpdated = 0
 | 
				
			||||||
  for (const library of libraries) {
 | 
					  for (const library of libraries) {
 | 
				
			||||||
    // Find matching old library using exact match on folder paths, exact match on library name
 | 
					    // Find matching old library using exact match on folder paths, exact match on library name
 | 
				
			||||||
    const matchingOldLibrary = oldLibraries.find(ol => {
 | 
					    const matchingOldLibrary = oldLibraries.find((ol) => {
 | 
				
			||||||
      if (ol.name !== library.name) {
 | 
					      if (ol.name !== library.name) {
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const folderPaths = ol.folders?.map(f => f.fullPath) || []
 | 
					      const folderPaths = ol.folders?.map((f) => f.fullPath) || []
 | 
				
			||||||
      return folderPaths.join(',') === library.folderPaths.join(',')
 | 
					      return folderPaths.join(',') === library.folderPaths.join(',')
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1264,42 +1283,51 @@ async function handleOldLibraries(ctx) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Migration from 2.3.0 to 2.3.1 - fixing librariesAccessible and bookmarks
 | 
					 * Migration from 2.3.0 to 2.3.1 - fixing librariesAccessible and bookmarks
 | 
				
			||||||
 * @param {/src/Database} ctx 
 | 
					 * @param {import('../../Database')} ctx
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function handleOldUsers(ctx) {
 | 
					async function handleOldUsers(ctx) {
 | 
				
			||||||
  const users = await ctx.models.user.getOldUsers()
 | 
					  const usersNew = await ctx.userModel.findAll({
 | 
				
			||||||
 | 
					    include: ctx.models.mediaProgress
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  const users = usersNew.map((u) => ctx.userModel.getOldUser(u))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let usersUpdated = 0
 | 
					  let usersUpdated = 0
 | 
				
			||||||
  for (const user of users) {
 | 
					  for (const user of users) {
 | 
				
			||||||
    let hasUpdates = false
 | 
					    let hasUpdates = false
 | 
				
			||||||
    if (user.bookmarks?.length) {
 | 
					    if (user.bookmarks?.length) {
 | 
				
			||||||
      user.bookmarks = user.bookmarks.map(bm => {
 | 
					      user.bookmarks = user.bookmarks
 | 
				
			||||||
 | 
					        .map((bm) => {
 | 
				
			||||||
          // Only update if this is not the old id format
 | 
					          // Only update if this is not the old id format
 | 
				
			||||||
          if (!bm.libraryItemId.startsWith('li_')) return bm
 | 
					          if (!bm.libraryItemId.startsWith('li_')) return bm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
 | 
					          bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
 | 
				
			||||||
          hasUpdates = true
 | 
					          hasUpdates = true
 | 
				
			||||||
          return bm
 | 
					          return bm
 | 
				
			||||||
      }).filter(bm => bm.libraryItemId)
 | 
					        })
 | 
				
			||||||
 | 
					        .filter((bm) => bm.libraryItemId)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Convert old library ids to new library ids
 | 
					    // Convert old library ids to new library ids
 | 
				
			||||||
    if (user.librariesAccessible?.length) {
 | 
					    if (user.librariesAccessible?.length) {
 | 
				
			||||||
      user.librariesAccessible = user.librariesAccessible.map(lid => {
 | 
					      user.librariesAccessible = user.librariesAccessible
 | 
				
			||||||
 | 
					        .map((lid) => {
 | 
				
			||||||
          if (!lid.startsWith('lib_') && lid !== 'main') return lid // Already not an old library id so dont change
 | 
					          if (!lid.startsWith('lib_') && lid !== 'main') return lid // Already not an old library id so dont change
 | 
				
			||||||
          hasUpdates = true
 | 
					          hasUpdates = true
 | 
				
			||||||
          return oldDbIdMap.libraries[lid]
 | 
					          return oldDbIdMap.libraries[lid]
 | 
				
			||||||
      }).filter(lid => lid)
 | 
					        })
 | 
				
			||||||
 | 
					        .filter((lid) => lid)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (user.seriesHideFromContinueListening?.length) {
 | 
					    if (user.seriesHideFromContinueListening?.length) {
 | 
				
			||||||
      user.seriesHideFromContinueListening = user.seriesHideFromContinueListening.map((seriesId) => {
 | 
					      user.seriesHideFromContinueListening = user.seriesHideFromContinueListening
 | 
				
			||||||
 | 
					        .map((seriesId) => {
 | 
				
			||||||
          if (seriesId.startsWith('se_')) {
 | 
					          if (seriesId.startsWith('se_')) {
 | 
				
			||||||
            hasUpdates = true
 | 
					            hasUpdates = true
 | 
				
			||||||
            return null // Filter out old series ids
 | 
					            return null // Filter out old series ids
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          return seriesId
 | 
					          return seriesId
 | 
				
			||||||
      }).filter(se => se)
 | 
					        })
 | 
				
			||||||
 | 
					        .filter((se) => se)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (hasUpdates) {
 | 
					    if (hasUpdates) {
 | 
				
			||||||
@ -1328,7 +1356,7 @@ module.exports.migrationPatch = async (ctx) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
 | 
					  const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
 | 
				
			||||||
  if (!await fs.pathExists(oldDbPath)) {
 | 
					  if (!(await fs.pathExists(oldDbPath))) {
 | 
				
			||||||
    Logger.info(`[dbMigration] Migration patch 2.3.0+ unnecessary - no oldDb.zip found`)
 | 
					    Logger.info(`[dbMigration] Migration patch 2.3.0+ unnecessary - no oldDb.zip found`)
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -1337,7 +1365,7 @@ module.exports.migrationPatch = async (ctx) => {
 | 
				
			|||||||
  Logger.info(`[dbMigration] Applying migration patch from 2.3.0+`)
 | 
					  Logger.info(`[dbMigration] Applying migration patch from 2.3.0+`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Extract from oldDb.zip
 | 
					  // Extract from oldDb.zip
 | 
				
			||||||
  if (!await oldDbFiles.checkExtractItemsUsersAndLibraries()) {
 | 
					  if (!(await oldDbFiles.checkExtractItemsUsersAndLibraries())) {
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1368,7 +1396,7 @@ async function migrationPatch2LibraryItems(ctx, offset = 0) {
 | 
				
			|||||||
  for (const libraryItem of libraryItems) {
 | 
					  for (const libraryItem of libraryItems) {
 | 
				
			||||||
    if (libraryItem.libraryFiles?.length) {
 | 
					    if (libraryItem.libraryFiles?.length) {
 | 
				
			||||||
      let size = 0
 | 
					      let size = 0
 | 
				
			||||||
      libraryItem.libraryFiles.forEach(lf => {
 | 
					      libraryItem.libraryFiles.forEach((lf) => {
 | 
				
			||||||
        if (!isNaN(lf.metadata?.size)) {
 | 
					        if (!isNaN(lf.metadata?.size)) {
 | 
				
			||||||
          size += Number(lf.metadata.size)
 | 
					          size += Number(lf.metadata.size)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -1411,7 +1439,7 @@ async function migrationPatch2Books(ctx, offset = 0) {
 | 
				
			|||||||
    let duration = 0
 | 
					    let duration = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (book.audioFiles?.length) {
 | 
					    if (book.audioFiles?.length) {
 | 
				
			||||||
      const tracks = book.audioFiles.filter(af => !af.exclude && !af.invalid)
 | 
					      const tracks = book.audioFiles.filter((af) => !af.exclude && !af.invalid)
 | 
				
			||||||
      for (const track of tracks) {
 | 
					      for (const track of tracks) {
 | 
				
			||||||
        if (track.duration !== null && !isNaN(track.duration)) {
 | 
					        if (track.duration !== null && !isNaN(track.duration)) {
 | 
				
			||||||
          duration += track.duration
 | 
					          duration += track.duration
 | 
				
			||||||
@ -1631,44 +1659,95 @@ module.exports.migrationPatch2 = async (ctx) => {
 | 
				
			|||||||
  Logger.info(`[dbMigration] Applying migration patch from 2.3.3+`)
 | 
					  Logger.info(`[dbMigration] Applying migration patch from 2.3.3+`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    await queryInterface.sequelize.transaction(t => {
 | 
					    await queryInterface.sequelize.transaction((t) => {
 | 
				
			||||||
      const queries = []
 | 
					      const queries = []
 | 
				
			||||||
      if (!bookAuthorsTableDescription?.createdAt) {
 | 
					      if (!bookAuthorsTableDescription?.createdAt) {
 | 
				
			||||||
        queries.push(...[
 | 
					        queries.push(
 | 
				
			||||||
          queryInterface.addColumn('bookAuthors', 'createdAt', {
 | 
					          ...[
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'bookAuthors',
 | 
				
			||||||
 | 
					              'createdAt',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.DATE
 | 
					                type: DataTypes.DATE
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('bookSeries', 'createdAt', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'bookSeries',
 | 
				
			||||||
 | 
					              'createdAt',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.DATE
 | 
					                type: DataTypes.DATE
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
        ])
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (!authorsTableDescription?.lastFirst) {
 | 
					      if (!authorsTableDescription?.lastFirst) {
 | 
				
			||||||
        queries.push(...[
 | 
					        queries.push(
 | 
				
			||||||
          queryInterface.addColumn('authors', 'lastFirst', {
 | 
					          ...[
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'authors',
 | 
				
			||||||
 | 
					              'lastFirst',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.STRING
 | 
					                type: DataTypes.STRING
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('libraryItems', 'size', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'libraryItems',
 | 
				
			||||||
 | 
					              'size',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.BIGINT
 | 
					                type: DataTypes.BIGINT
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('books', 'duration', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'books',
 | 
				
			||||||
 | 
					              'duration',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.FLOAT
 | 
					                type: DataTypes.FLOAT
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('books', 'titleIgnorePrefix', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'books',
 | 
				
			||||||
 | 
					              'titleIgnorePrefix',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.STRING
 | 
					                type: DataTypes.STRING
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('podcasts', 'titleIgnorePrefix', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'podcasts',
 | 
				
			||||||
 | 
					              'titleIgnorePrefix',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.STRING
 | 
					                type: DataTypes.STRING
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
          queryInterface.addColumn('series', 'nameIgnorePrefix', {
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            queryInterface.addColumn(
 | 
				
			||||||
 | 
					              'series',
 | 
				
			||||||
 | 
					              'nameIgnorePrefix',
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
                type: DataTypes.STRING
 | 
					                type: DataTypes.STRING
 | 
				
			||||||
          }, { transaction: t }),
 | 
					              },
 | 
				
			||||||
        ])
 | 
					              { transaction: t }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (!feedTableDescription?.coverPath) {
 | 
					      if (!feedTableDescription?.coverPath) {
 | 
				
			||||||
        queries.push(queryInterface.addColumn('feeds', 'coverPath', {
 | 
					        queries.push(
 | 
				
			||||||
 | 
					          queryInterface.addColumn(
 | 
				
			||||||
 | 
					            'feeds',
 | 
				
			||||||
 | 
					            'coverPath',
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
              type: DataTypes.STRING
 | 
					              type: DataTypes.STRING
 | 
				
			||||||
        }, { transaction: t }))
 | 
					            },
 | 
				
			||||||
 | 
					            { transaction: t }
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return Promise.all(queries)
 | 
					      return Promise.all(queries)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user