mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-24 01:13:00 -04:00
Init sqlite take 2
This commit is contained in:
parent
d86a3b3dc2
commit
cf7fd315b6
@ -154,7 +154,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.deletingDeviceName = device.name
|
this.deletingDeviceName = device.name
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/emails/ereader-devices`, payload)
|
.$post(`/api/emails/ereader-devices`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||||
this.$toast.success('Device deleted')
|
this.$toast.success('Device deleted')
|
||||||
|
@ -191,6 +191,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
|
clientName: 'Abs Web',
|
||||||
deviceId: this.getDeviceId()
|
deviceId: this.getDeviceId()
|
||||||
},
|
},
|
||||||
supportedMimeTypes: this.player.playableMimeTypes,
|
supportedMimeTypes: this.player.playableMimeTypes,
|
||||||
|
2376
package-lock.json
generated
2376
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,9 @@
|
|||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
"nodemailer": "^6.9.2",
|
"nodemailer": "^6.9.2",
|
||||||
|
"sequelize": "^6.32.1",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -2,21 +2,10 @@ const bcrypt = require('./libs/bcryptjs')
|
|||||||
const jwt = require('./libs/jsonwebtoken')
|
const jwt = require('./libs/jsonwebtoken')
|
||||||
const requestIp = require('./libs/requestIp')
|
const requestIp = require('./libs/requestIp')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const Database = require('./Database')
|
||||||
|
|
||||||
class Auth {
|
class Auth {
|
||||||
constructor(db) {
|
constructor() { }
|
||||||
this.db = db
|
|
||||||
|
|
||||||
this.user = null
|
|
||||||
}
|
|
||||||
|
|
||||||
get username() {
|
|
||||||
return this.user ? this.user.username : 'nobody'
|
|
||||||
}
|
|
||||||
|
|
||||||
get users() {
|
|
||||||
return this.db.users
|
|
||||||
}
|
|
||||||
|
|
||||||
cors(req, res, next) {
|
cors(req, res, next) {
|
||||||
res.header('Access-Control-Allow-Origin', '*')
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
@ -35,20 +24,20 @@ class Auth {
|
|||||||
async initTokenSecret() {
|
async initTokenSecret() {
|
||||||
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
||||||
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
||||||
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
||||||
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||||
}
|
}
|
||||||
await this.db.updateServerSettings()
|
await Database.updateServerSettings()
|
||||||
|
|
||||||
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||||
if (this.db.users.length) {
|
if (Database.users.length) {
|
||||||
for (const user of this.db.users) {
|
for (const user of Database.users) {
|
||||||
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
||||||
}
|
}
|
||||||
await this.db.updateEntities('user', this.db.users)
|
await Database.updateBulkUsers(Database.users)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +57,7 @@ class Auth {
|
|||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await this.verifyToken(token)
|
const user = await this.verifyToken(token)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
Logger.error('Verify Token User Not Found', token)
|
Logger.error('Verify Token User Not Found', token)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@ -95,7 +84,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateAccessToken(payload) {
|
generateAccessToken(payload) {
|
||||||
return jwt.sign(payload, global.ServerSettings.tokenSecret);
|
return jwt.sign(payload, Database.serverSettings.tokenSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateUser(token) {
|
authenticateUser(token) {
|
||||||
@ -104,12 +93,12 @@ class Auth {
|
|||||||
|
|
||||||
verifyToken(token) {
|
verifyToken(token) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => {
|
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
|
||||||
if (!payload || err) {
|
if (!payload || err) {
|
||||||
Logger.error('JWT Verify Token Failed', err)
|
Logger.error('JWT Verify Token Failed', err)
|
||||||
return resolve(null)
|
return resolve(null)
|
||||||
}
|
}
|
||||||
const user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username)
|
||||||
resolve(user || null)
|
resolve(user || null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -118,9 +107,9 @@ class Auth {
|
|||||||
getUserLoginResponsePayload(user) {
|
getUserLoginResponsePayload(user) {
|
||||||
return {
|
return {
|
||||||
user: user.toJSONForBrowser(),
|
user: user.toJSONForBrowser(),
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
|
||||||
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||||
ereaderDevices: this.db.emailSettings.getEReaderDevices(user),
|
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +119,7 @@ class Auth {
|
|||||||
const username = (req.body.username || '').toLowerCase()
|
const username = (req.body.username || '').toLowerCase()
|
||||||
const password = req.body.password || ''
|
const password = req.body.password || ''
|
||||||
|
|
||||||
const user = this.users.find(u => u.username.toLowerCase() === username)
|
const user = Database.users.find(u => u.username.toLowerCase() === username)
|
||||||
|
|
||||||
if (!user?.isActive) {
|
if (!user?.isActive) {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
@ -142,7 +131,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check passwordless root user
|
// Check passwordless root user
|
||||||
if (user.id === 'root' && (!user.pash || user.pash === '')) {
|
if (user.type === 'root' && (!user.pash || user.pash === '')) {
|
||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
@ -166,15 +155,6 @@ class Auth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not in use now
|
|
||||||
lockUser(user) {
|
|
||||||
user.isLocked = true
|
|
||||||
return this.db.updateEntity('user', user).catch((error) => {
|
|
||||||
Logger.error('[Auth] Failed to lock user', user.username, error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
comparePassword(password, user) {
|
comparePassword(password, user) {
|
||||||
if (user.type === 'root' && !password && !user.pash) return true
|
if (user.type === 'root' && !password && !user.pash) return true
|
||||||
if (!password || !user.pash) return false
|
if (!password || !user.pash) return false
|
||||||
@ -184,7 +164,7 @@ class Auth {
|
|||||||
async userChangePassword(req, res) {
|
async userChangePassword(req, res) {
|
||||||
var { password, newPassword } = req.body
|
var { password, newPassword } = req.body
|
||||||
newPassword = newPassword || ''
|
newPassword = newPassword || ''
|
||||||
var matchingUser = this.users.find(u => u.id === req.user.id)
|
const matchingUser = Database.users.find(u => u.id === req.user.id)
|
||||||
|
|
||||||
// Only root can have an empty password
|
// Only root can have an empty password
|
||||||
if (matchingUser.type !== 'root' && !newPassword) {
|
if (matchingUser.type !== 'root' && !newPassword) {
|
||||||
@ -193,14 +173,14 @@ class Auth {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var compare = await this.comparePassword(password, matchingUser)
|
const compare = await this.comparePassword(password, matchingUser)
|
||||||
if (!compare) {
|
if (!compare) {
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Invalid password'
|
error: 'Invalid password'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var pw = ''
|
let pw = ''
|
||||||
if (newPassword) {
|
if (newPassword) {
|
||||||
pw = await this.hashPass(newPassword)
|
pw = await this.hashPass(newPassword)
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
@ -211,7 +191,8 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
matchingUser.pash = pw
|
matchingUser.pash = pw
|
||||||
var success = await this.db.updateEntity('user', matchingUser)
|
|
||||||
|
const success = await Database.updateUser(matchingUser)
|
||||||
if (success) {
|
if (success) {
|
||||||
res.json({
|
res.json({
|
||||||
success: true
|
success: true
|
||||||
|
456
server/Database.js
Normal file
456
server/Database.js
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const { Sequelize } = require('sequelize')
|
||||||
|
|
||||||
|
const packageJson = require('../package.json')
|
||||||
|
const fs = require('./libs/fsExtra')
|
||||||
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
|
const dbMigration = require('./utils/migrations/dbMigration')
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
constructor() {
|
||||||
|
this.sequelize = null
|
||||||
|
this.dbPath = null
|
||||||
|
this.isNew = false // New database.sqlite created
|
||||||
|
|
||||||
|
// Temporarily using format of old DB
|
||||||
|
// below data should be loaded from the DB as needed
|
||||||
|
this.libraryItems = []
|
||||||
|
this.users = []
|
||||||
|
this.libraries = []
|
||||||
|
this.settings = []
|
||||||
|
this.collections = []
|
||||||
|
this.playlists = []
|
||||||
|
this.authors = []
|
||||||
|
this.series = []
|
||||||
|
this.feeds = []
|
||||||
|
|
||||||
|
this.serverSettings = null
|
||||||
|
this.notificationSettings = null
|
||||||
|
this.emailSettings = null
|
||||||
|
}
|
||||||
|
|
||||||
|
get models() {
|
||||||
|
return this.sequelize?.models || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasRootUser() {
|
||||||
|
return this.users.some(u => u.type === 'root')
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkHasDb() {
|
||||||
|
if (!await fs.pathExists(this.dbPath)) {
|
||||||
|
Logger.info(`[Database] database.sqlite not found at ${this.dbPath}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(force = false) {
|
||||||
|
this.dbPath = Path.join(global.ConfigPath, 'database.sqlite')
|
||||||
|
|
||||||
|
// First check if this is a new database
|
||||||
|
this.isNew = !(await this.checkHasDb()) || force
|
||||||
|
|
||||||
|
if (!await this.connect()) {
|
||||||
|
throw new Error('Database connection failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.buildModels(force)
|
||||||
|
Logger.info(`[Database] Db initialized`, Object.keys(this.sequelize.models))
|
||||||
|
|
||||||
|
await this.loadData(force)
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
Logger.info(`[Database] Initializing db at "${this.dbPath}"`)
|
||||||
|
this.sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: this.dbPath,
|
||||||
|
logging: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
this.sequelize.uppercaseFirst = str => str ? `${str[0].toUpperCase()}${str.substr(1)}` : ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sequelize.authenticate()
|
||||||
|
Logger.info(`[Database] Db connection was successful`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[Database] Failed to connect to db`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildModels(force = false) {
|
||||||
|
require('./models/User')(this.sequelize)
|
||||||
|
require('./models/Library')(this.sequelize)
|
||||||
|
require('./models/LibraryFolder')(this.sequelize)
|
||||||
|
require('./models/Book')(this.sequelize)
|
||||||
|
require('./models/Podcast')(this.sequelize)
|
||||||
|
require('./models/PodcastEpisode')(this.sequelize)
|
||||||
|
require('./models/LibraryItem')(this.sequelize)
|
||||||
|
require('./models/MediaProgress')(this.sequelize)
|
||||||
|
require('./models/Series')(this.sequelize)
|
||||||
|
require('./models/BookSeries')(this.sequelize)
|
||||||
|
require('./models/Author')(this.sequelize)
|
||||||
|
require('./models/BookAuthor')(this.sequelize)
|
||||||
|
require('./models/Collection')(this.sequelize)
|
||||||
|
require('./models/CollectionBook')(this.sequelize)
|
||||||
|
require('./models/Playlist')(this.sequelize)
|
||||||
|
require('./models/PlaylistMediaItem')(this.sequelize)
|
||||||
|
require('./models/Device')(this.sequelize)
|
||||||
|
require('./models/PlaybackSession')(this.sequelize)
|
||||||
|
require('./models/Feed')(this.sequelize)
|
||||||
|
require('./models/FeedEpisode')(this.sequelize)
|
||||||
|
require('./models/Setting')(this.sequelize)
|
||||||
|
|
||||||
|
return this.sequelize.sync({ force })
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(force = false) {
|
||||||
|
if (this.isNew && await dbMigration.checkShouldMigrate(force)) {
|
||||||
|
Logger.info(`[Database] New database was created and old database was detected - migrating old to new`)
|
||||||
|
await dbMigration.migrate(this.models)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
this.libraryItems = await this.models.libraryItem.getAllOldLibraryItems()
|
||||||
|
this.users = await this.models.user.getOldUsers()
|
||||||
|
this.libraries = await this.models.library.getAllOldLibraries()
|
||||||
|
this.collections = await this.models.collection.getOldCollections()
|
||||||
|
this.playlists = await this.models.playlist.getOldPlaylists()
|
||||||
|
this.authors = await this.models.author.getOldAuthors()
|
||||||
|
this.series = await this.models.series.getAllOldSeries()
|
||||||
|
this.feeds = await this.models.feed.getOldFeeds()
|
||||||
|
|
||||||
|
const settingsData = await this.models.setting.getOldSettings()
|
||||||
|
this.settings = settingsData.settings
|
||||||
|
this.emailSettings = settingsData.emailSettings
|
||||||
|
this.serverSettings = settingsData.serverSettings
|
||||||
|
this.notificationSettings = settingsData.notificationSettings
|
||||||
|
global.ServerSettings = this.serverSettings.toJSON()
|
||||||
|
|
||||||
|
Logger.info(`[Database] Db data loaded in ${Date.now() - startTime}ms`)
|
||||||
|
|
||||||
|
if (packageJson.version !== this.serverSettings.version) {
|
||||||
|
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
||||||
|
this.serverSettings.version = packageJson.version
|
||||||
|
await this.updateServerSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRootUser(username, pash, token) {
|
||||||
|
const newUser = await this.models.user.createRootUser(username, pash, token)
|
||||||
|
if (newUser) {
|
||||||
|
this.users.push(newUser)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateServerSettings() {
|
||||||
|
global.ServerSettings = this.serverSettings.toJSON()
|
||||||
|
return this.updateSetting(this.serverSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSetting(settings) {
|
||||||
|
return this.models.setting.updateSettingObj(settings.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(oldUser) {
|
||||||
|
await this.models.user.createFromOld(oldUser)
|
||||||
|
this.users.push(oldUser)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(oldUser) {
|
||||||
|
return this.models.user.updateFromOld(oldUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBulkUsers(oldUsers) {
|
||||||
|
return Promise.all(oldUsers.map(u => this.updateUser(u)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeUser(userId) {
|
||||||
|
await this.models.user.removeById(userId)
|
||||||
|
this.users = this.users.filter(u => u.id !== userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertMediaProgress(oldMediaProgress) {
|
||||||
|
return this.models.mediaProgress.upsertFromOld(oldMediaProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMediaProgress(mediaProgressId) {
|
||||||
|
return this.models.mediaProgress.removeById(mediaProgressId)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBulkBooks(oldBooks) {
|
||||||
|
return Promise.all(oldBooks.map(oldBook => this.models.book.saveFromOld(oldBook)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLibrary(oldLibrary) {
|
||||||
|
await this.models.library.createFromOld(oldLibrary)
|
||||||
|
this.libraries.push(oldLibrary)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLibrary(oldLibrary) {
|
||||||
|
return this.models.library.updateFromOld(oldLibrary)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLibrary(libraryId) {
|
||||||
|
await this.models.library.removeById(libraryId)
|
||||||
|
this.libraries = this.libraries.filter(lib => lib.id !== libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCollection(oldCollection) {
|
||||||
|
const newCollection = await this.models.collection.createFromOld(oldCollection)
|
||||||
|
// Create CollectionBooks
|
||||||
|
if (newCollection) {
|
||||||
|
const collectionBooks = []
|
||||||
|
oldCollection.books.forEach((libraryItemId) => {
|
||||||
|
const libraryItem = this.libraryItems.filter(li => li.id === libraryItemId)
|
||||||
|
if (libraryItem) {
|
||||||
|
collectionBooks.push({
|
||||||
|
collectionId: newCollection.id,
|
||||||
|
bookId: libraryItem.media.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (collectionBooks.length) {
|
||||||
|
await this.createBulkCollectionBooks(collectionBooks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.collections.push(oldCollection)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCollection(oldCollection) {
|
||||||
|
const collectionBooks = []
|
||||||
|
let order = 1
|
||||||
|
oldCollection.books.forEach((libraryItemId) => {
|
||||||
|
const libraryItem = this.getLibraryItem(libraryItemId)
|
||||||
|
if (!libraryItem) return
|
||||||
|
collectionBooks.push({
|
||||||
|
collectionId: oldCollection.id,
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
order: order++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return this.models.collection.fullUpdateFromOld(oldCollection, collectionBooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCollection(collectionId) {
|
||||||
|
await this.models.collection.removeById(collectionId)
|
||||||
|
this.collections = this.collections.filter(c => c.id !== collectionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createCollectionBook(collectionBook) {
|
||||||
|
return this.models.collectionBook.create(collectionBook)
|
||||||
|
}
|
||||||
|
|
||||||
|
createBulkCollectionBooks(collectionBooks) {
|
||||||
|
return this.models.collectionBook.bulkCreate(collectionBooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCollectionBook(collectionId, bookId) {
|
||||||
|
return this.models.collectionBook.removeByIds(collectionId, bookId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPlaylist(oldPlaylist) {
|
||||||
|
const newPlaylist = await this.models.playlist.createFromOld(oldPlaylist)
|
||||||
|
if (newPlaylist) {
|
||||||
|
const playlistMediaItems = []
|
||||||
|
let order = 1
|
||||||
|
for (const mediaItemObj of oldPlaylist.items) {
|
||||||
|
const libraryItem = this.libraryItems.find(li => li.id === mediaItemObj.libraryItemId)
|
||||||
|
if (!libraryItem) continue
|
||||||
|
|
||||||
|
let mediaItemId = libraryItem.media.id // bookId
|
||||||
|
let mediaItemType = 'book'
|
||||||
|
if (mediaItemObj.episodeId) {
|
||||||
|
mediaItemType = 'podcastEpisode'
|
||||||
|
mediaItemId = mediaItemObj.episodeId
|
||||||
|
}
|
||||||
|
playlistMediaItems.push({
|
||||||
|
playlistId: newPlaylist.id,
|
||||||
|
mediaItemId,
|
||||||
|
mediaItemType,
|
||||||
|
order: order++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (playlistMediaItems.length) {
|
||||||
|
await this.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.playlists.push(oldPlaylist)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlaylist(oldPlaylist) {
|
||||||
|
const playlistMediaItems = []
|
||||||
|
let order = 1
|
||||||
|
oldPlaylist.items.forEach((item) => {
|
||||||
|
const libraryItem = this.getLibraryItem(item.libraryItemId)
|
||||||
|
if (!libraryItem) return
|
||||||
|
playlistMediaItems.push({
|
||||||
|
playlistId: oldPlaylist.id,
|
||||||
|
mediaItemId: item.episodeId || libraryItem.media.id,
|
||||||
|
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||||
|
order: order++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return this.models.playlist.fullUpdateFromOld(oldPlaylist, playlistMediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePlaylist(playlistId) {
|
||||||
|
await this.models.playlist.removeById(playlistId)
|
||||||
|
this.playlists = this.playlists.filter(p => p.id !== playlistId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlaylistMediaItem(playlistMediaItem) {
|
||||||
|
return this.models.playlistMediaItem.create(playlistMediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
createBulkPlaylistMediaItems(playlistMediaItems) {
|
||||||
|
return this.models.playlistMediaItem.bulkCreate(playlistMediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlaylistMediaItem(playlistId, mediaItemId) {
|
||||||
|
return this.models.playlistMediaItem.removeByIds(playlistId, mediaItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryItem(libraryItemId) {
|
||||||
|
return this.libraryItems.find(li => li.id === libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLibraryItem(oldLibraryItem) {
|
||||||
|
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||||
|
this.libraryItems.push(oldLibraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLibraryItem(oldLibraryItem) {
|
||||||
|
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBulkLibraryItems(oldLibraryItems) {
|
||||||
|
let updatesMade = 0
|
||||||
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
|
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||||
|
if (hasUpdates) updatesMade++
|
||||||
|
}
|
||||||
|
return updatesMade
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBulkLibraryItems(oldLibraryItems) {
|
||||||
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
|
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||||
|
this.libraryItems.push(oldLibraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLibraryItem(libraryItemId) {
|
||||||
|
await this.models.libraryItem.removeById(libraryItemId)
|
||||||
|
this.libraryItems = this.libraryItems.filter(li => li.id !== libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createFeed(oldFeed) {
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeed(oldFeed) {
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFeed(feedId) {
|
||||||
|
await this.models.feed.removeById(feedId)
|
||||||
|
this.feeds = this.feeds.filter(f => f.id !== feedId)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSeries(oldSeries) {
|
||||||
|
return this.models.series.updateFromOld(oldSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSeries(oldSeries) {
|
||||||
|
await this.models.series.createFromOld(oldSeries)
|
||||||
|
this.series.push(oldSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBulkSeries(oldSeriesObjs) {
|
||||||
|
await this.models.series.createBulkFromOld(oldSeriesObjs)
|
||||||
|
this.series.push(...oldSeriesObjs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSeries(seriesId) {
|
||||||
|
await this.models.series.removeById(seriesId)
|
||||||
|
this.series = this.series.filter(se => se.id !== seriesId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAuthor(oldAuthor) {
|
||||||
|
await this.models.createFromOld(oldAuthor)
|
||||||
|
this.authors.push(oldAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBulkAuthors(oldAuthors) {
|
||||||
|
await this.models.author.createBulkFromOld(oldAuthors)
|
||||||
|
this.authors.push(...oldAuthors)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAuthor(oldAuthor) {
|
||||||
|
return this.models.author.updateFromOld(oldAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAuthor(authorId) {
|
||||||
|
await this.models.author.removeById(authorId)
|
||||||
|
this.authors = this.authors.filter(au => au.id !== authorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBulkBookAuthors(bookAuthors) {
|
||||||
|
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
||||||
|
this.authors.push(...bookAuthors)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeBulkBookAuthors(authorId = null, bookId = null) {
|
||||||
|
if (!authorId && !bookId) return
|
||||||
|
await this.models.bookAuthor.removeByIds(authorId, bookId)
|
||||||
|
this.authors = this.authors.filter(au => {
|
||||||
|
if (authorId && au.authorId !== authorId) return true
|
||||||
|
if (bookId && au.bookId !== bookId) return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaybackSessions(where = null) {
|
||||||
|
return this.models.playbackSession.getOldPlaybackSessions(where)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaybackSession(sessionId) {
|
||||||
|
return this.models.playbackSession.getById(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlaybackSession(oldSession) {
|
||||||
|
return this.models.playbackSession.createFromOld(oldSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlaybackSession(oldSession) {
|
||||||
|
return this.models.playbackSession.updateFromOld(oldSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlaybackSession(sessionId) {
|
||||||
|
return this.models.playbackSession.removeById(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceByDeviceId(deviceId) {
|
||||||
|
return this.models.device.getOldDeviceByDeviceId(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDevice(oldDevice) {
|
||||||
|
return this.models.device.updateFromOld(oldDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
createDevice(oldDevice) {
|
||||||
|
return this.models.device.createFromOld(oldDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Database()
|
49
server/Db.js
49
server/Db.js
@ -243,9 +243,6 @@ class Db {
|
|||||||
getLibraryItem(id) {
|
getLibraryItem(id) {
|
||||||
return this.libraryItems.find(li => li.id === id)
|
return this.libraryItems.find(li => li.id === id)
|
||||||
}
|
}
|
||||||
getLibraryItemsInLibrary(libraryId) {
|
|
||||||
return this.libraryItems.filter(li => li.libraryId === libraryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateLibraryItem(libraryItem) {
|
async updateLibraryItem(libraryItem) {
|
||||||
return this.updateLibraryItems([libraryItem])
|
return this.updateLibraryItems([libraryItem])
|
||||||
@ -269,26 +266,6 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertLibraryItem(libraryItem) {
|
|
||||||
return this.insertLibraryItems([libraryItem])
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertLibraryItems(libraryItems) {
|
|
||||||
await Promise.all(libraryItems.map(async (li) => {
|
|
||||||
if (li && li.saveMetadata) return li.saveMetadata()
|
|
||||||
return null
|
|
||||||
}))
|
|
||||||
|
|
||||||
return this.libraryItemsDb.insert(libraryItems).then((results) => {
|
|
||||||
Logger.debug(`[DB] Library Items inserted ${results.inserted}`)
|
|
||||||
this.libraryItems = this.libraryItems.concat(libraryItems)
|
|
||||||
return true
|
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error(`[DB] Library Items insert failed ${error}`)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
removeLibraryItem(id) {
|
removeLibraryItem(id) {
|
||||||
return this.libraryItemsDb.delete((record) => record.id === id).then((results) => {
|
return this.libraryItemsDb.delete((record) => record.id === id).then((results) => {
|
||||||
Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`)
|
Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`)
|
||||||
@ -303,14 +280,6 @@ class Db {
|
|||||||
return this.updateEntity('settings', this.serverSettings)
|
return this.updateEntity('settings', this.serverSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllEntities(entityName) {
|
|
||||||
const entityDb = this.getEntityDb(entityName)
|
|
||||||
return entityDb.select(() => true).then((results) => results.data).catch((error) => {
|
|
||||||
Logger.error(`[DB] Failed to get all ${entityName}`, error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
insertEntities(entityName, entities) {
|
insertEntities(entityName, entities) {
|
||||||
var entityDb = this.getEntityDb(entityName)
|
var entityDb = this.getEntityDb(entityName)
|
||||||
return entityDb.insert(entities).then((results) => {
|
return entityDb.insert(entities).then((results) => {
|
||||||
@ -463,15 +432,6 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllSessions(selectFunc = () => true) {
|
|
||||||
return this.sessionsDb.select(selectFunc).then((results) => {
|
|
||||||
return results.data || []
|
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error('[Db] Failed to select sessions', error)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlaybackSession(id) {
|
getPlaybackSession(id) {
|
||||||
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
|
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
|
||||||
if (results.data.length) {
|
if (results.data.length) {
|
||||||
@ -484,15 +444,6 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
selectUserSessions(userId) {
|
|
||||||
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
|
||||||
return results.data || []
|
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error(`[Db] Failed to select user sessions "${userId}"`, error)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if server was updated and previous version was earlier than param
|
// Check if server was updated and previous version was earlier than param
|
||||||
checkPreviousVersionIsBefore(version) {
|
checkPreviousVersionIsBefore(version) {
|
||||||
if (!this.previousVersion) return false
|
if (!this.previousVersion) return false
|
||||||
|
105
server/Server.js
105
server/Server.js
@ -8,18 +8,18 @@ const rateLimit = require('./libs/expressRateLimit')
|
|||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
const dbMigration = require('./utils/dbMigration')
|
|
||||||
const filePerms = require('./utils/filePerms')
|
const filePerms = require('./utils/filePerms')
|
||||||
const fileUtils = require('./utils/fileUtils')
|
const fileUtils = require('./utils/fileUtils')
|
||||||
const globals = require('./utils/globals')
|
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
const Auth = require('./Auth')
|
const Auth = require('./Auth')
|
||||||
const Watcher = require('./Watcher')
|
const Watcher = require('./Watcher')
|
||||||
const Scanner = require('./scanner/Scanner')
|
const Scanner = require('./scanner/Scanner')
|
||||||
const Db = require('./Db')
|
const Database = require('./Database')
|
||||||
const SocketAuthority = require('./SocketAuthority')
|
const SocketAuthority = require('./SocketAuthority')
|
||||||
|
|
||||||
|
const routes = require('./routes/index')
|
||||||
|
|
||||||
const ApiRouter = require('./routers/ApiRouter')
|
const ApiRouter = require('./routers/ApiRouter')
|
||||||
const HlsRouter = require('./routers/HlsRouter')
|
const HlsRouter = require('./routers/HlsRouter')
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ const CoverManager = require('./managers/CoverManager')
|
|||||||
const AbMergeManager = require('./managers/AbMergeManager')
|
const AbMergeManager = require('./managers/AbMergeManager')
|
||||||
const CacheManager = require('./managers/CacheManager')
|
const CacheManager = require('./managers/CacheManager')
|
||||||
const LogManager = require('./managers/LogManager')
|
const LogManager = require('./managers/LogManager')
|
||||||
const BackupManager = require('./managers/BackupManager')
|
// const BackupManager = require('./managers/BackupManager') // TODO
|
||||||
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
||||||
const PodcastManager = require('./managers/PodcastManager')
|
const PodcastManager = require('./managers/PodcastManager')
|
||||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||||
@ -59,30 +59,30 @@ class Server {
|
|||||||
filePerms.setDefaultDirSync(global.MetadataPath, false)
|
filePerms.setDefaultDirSync(global.MetadataPath, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.db = new Db()
|
// this.db = new Db()
|
||||||
this.watcher = new Watcher()
|
this.watcher = new Watcher()
|
||||||
this.auth = new Auth(this.db)
|
this.auth = new Auth()
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
this.taskManager = new TaskManager()
|
this.taskManager = new TaskManager()
|
||||||
this.notificationManager = new NotificationManager(this.db)
|
this.notificationManager = new NotificationManager()
|
||||||
this.emailManager = new EmailManager(this.db)
|
this.emailManager = new EmailManager()
|
||||||
this.backupManager = new BackupManager(this.db)
|
// this.backupManager = new BackupManager(this.db)
|
||||||
this.logManager = new LogManager(this.db)
|
this.logManager = new LogManager()
|
||||||
this.cacheManager = new CacheManager()
|
this.cacheManager = new CacheManager()
|
||||||
this.abMergeManager = new AbMergeManager(this.db, this.taskManager)
|
this.abMergeManager = new AbMergeManager(this.taskManager)
|
||||||
this.playbackSessionManager = new PlaybackSessionManager(this.db)
|
this.playbackSessionManager = new PlaybackSessionManager()
|
||||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
this.coverManager = new CoverManager(this.cacheManager)
|
||||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager)
|
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager)
|
||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager)
|
||||||
this.rssFeedManager = new RssFeedManager(this.db)
|
this.rssFeedManager = new RssFeedManager()
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager, this.taskManager)
|
this.scanner = new Scanner(this.coverManager, this.taskManager)
|
||||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
this.cronManager = new CronManager(this.scanner, this.podcastManager)
|
||||||
|
|
||||||
// Routers
|
// Routers
|
||||||
this.apiRouter = new ApiRouter(this)
|
this.apiRouter = new ApiRouter(this)
|
||||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
|
||||||
|
|
||||||
Logger.logManager = this.logManager
|
Logger.logManager = this.logManager
|
||||||
|
|
||||||
@ -98,38 +98,28 @@ class Server {
|
|||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
await this.playbackSessionManager.removeOrphanStreams()
|
await this.playbackSessionManager.removeOrphanStreams()
|
||||||
|
|
||||||
const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
await Database.init(false)
|
||||||
if (previousVersion) {
|
|
||||||
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
|
|
||||||
}
|
|
||||||
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
|
|
||||||
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
|
|
||||||
await dbMigration.migrate(this.db)
|
|
||||||
} else {
|
|
||||||
await this.db.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create token secret if does not exist (Added v2.1.0)
|
// Create token secret if does not exist (Added v2.1.0)
|
||||||
if (!this.db.serverSettings.tokenSecret) {
|
if (!Database.serverSettings.tokenSecret) {
|
||||||
await this.auth.initTokenSecret()
|
await this.auth.initTokenSecret()
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.cleanUserData() // Remove invalid user item progress
|
await this.cleanUserData() // Remove invalid user item progress
|
||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
await this.purgeMetadata() // Remove metadata folders without library item
|
||||||
await this.playbackSessionManager.removeInvalidSessions()
|
|
||||||
await this.cacheManager.ensureCachePaths()
|
await this.cacheManager.ensureCachePaths()
|
||||||
|
|
||||||
await this.backupManager.init()
|
// await this.backupManager.init() // TODO: Implement backups
|
||||||
await this.logManager.init()
|
await this.logManager.init()
|
||||||
await this.apiRouter.checkRemoveEmptySeries(this.db.series) // Remove empty series
|
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
this.cronManager.init()
|
this.cronManager.init()
|
||||||
|
|
||||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
if (Database.serverSettings.scannerDisableWatcher) {
|
||||||
Logger.info(`[Server] Watcher is disabled`)
|
Logger.info(`[Server] Watcher is disabled`)
|
||||||
this.watcher.disabled = true
|
this.watcher.disabled = true
|
||||||
} else {
|
} else {
|
||||||
this.watcher.initWatcher(this.db.libraries)
|
this.watcher.initWatcher(Database.libraries)
|
||||||
this.watcher.on('files', this.filesChanged.bind(this))
|
this.watcher.on('files', this.filesChanged.bind(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,6 +152,7 @@ class Server {
|
|||||||
// Static folder
|
// Static folder
|
||||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
|
// router.use('/api/v1', routes) // TODO: New routes
|
||||||
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
||||||
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||||
|
|
||||||
@ -203,7 +194,7 @@ class Server {
|
|||||||
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
||||||
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
router.post('/init', (req, res) => {
|
router.post('/init', (req, res) => {
|
||||||
if (this.db.hasRootUser) {
|
if (Database.hasRootUser) {
|
||||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
@ -213,8 +204,8 @@ class Server {
|
|||||||
// status check for client to see if server has been initialized
|
// status check for client to see if server has been initialized
|
||||||
// server has been initialized if a root user exists
|
// server has been initialized if a root user exists
|
||||||
const payload = {
|
const payload = {
|
||||||
isInit: this.db.hasRootUser,
|
isInit: Database.hasRootUser,
|
||||||
language: this.db.serverSettings.language
|
language: Database.serverSettings.language
|
||||||
}
|
}
|
||||||
if (!payload.isInit) {
|
if (!payload.isInit) {
|
||||||
payload.ConfigPath = global.ConfigPath
|
payload.ConfigPath = global.ConfigPath
|
||||||
@ -243,7 +234,7 @@ class Server {
|
|||||||
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
||||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||||
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
||||||
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
await Database.createRootUser(newRoot.username, rootPash, rootToken)
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -261,7 +252,7 @@ class Server {
|
|||||||
|
|
||||||
let purged = 0
|
let purged = 0
|
||||||
await Promise.all(foldersInItemsMetadata.map(async foldername => {
|
await Promise.all(foldersInItemsMetadata.map(async foldername => {
|
||||||
const hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
|
const hasMatchingItem = Database.libraryItems.find(ab => ab.id === foldername)
|
||||||
if (!hasMatchingItem) {
|
if (!hasMatchingItem) {
|
||||||
const folderPath = Path.join(itemsMetadata, foldername)
|
const folderPath = Path.join(itemsMetadata, foldername)
|
||||||
Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
|
Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
|
||||||
@ -281,26 +272,26 @@ class Server {
|
|||||||
|
|
||||||
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
||||||
async cleanUserData() {
|
async cleanUserData() {
|
||||||
for (let i = 0; i < this.db.users.length; i++) {
|
for (const _user of Database.users) {
|
||||||
const _user = this.db.users[i]
|
|
||||||
let hasUpdated = false
|
|
||||||
if (_user.mediaProgress.length) {
|
if (_user.mediaProgress.length) {
|
||||||
const lengthBefore = _user.mediaProgress.length
|
for (const mediaProgress of _user.mediaProgress) {
|
||||||
_user.mediaProgress = _user.mediaProgress.filter(mp => {
|
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
if (libraryItem && mediaProgress.episodeId) {
|
||||||
if (!libraryItem) return false
|
const episode = libraryItem.media.checkHasEpisode?.(mediaProgress.episodeId)
|
||||||
if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
|
if (episode) continue
|
||||||
return true
|
} else {
|
||||||
})
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (lengthBefore > _user.mediaProgress.length) {
|
Logger.debug(`[Server] Removing media progress ${mediaProgress.id} data from user ${_user.username}`)
|
||||||
Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`)
|
await Database.removeMediaProgress(mediaProgress.id)
|
||||||
hasUpdated = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hasUpdated = false
|
||||||
if (_user.seriesHideFromContinueListening.length) {
|
if (_user.seriesHideFromContinueListening.length) {
|
||||||
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
||||||
if (!this.db.series.some(se => se.id === seriesId)) { // Series removed
|
if (!Database.series.some(se => se.id === seriesId)) { // Series removed
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -308,7 +299,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('user', _user)
|
await Database.updateUser(_user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,8 +312,8 @@ class Server {
|
|||||||
|
|
||||||
getLoginRateLimiter() {
|
getLoginRateLimiter() {
|
||||||
return rateLimit({
|
return rateLimit({
|
||||||
windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes
|
windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
|
||||||
max: this.db.serverSettings.rateLimitLoginRequests,
|
max: Database.serverSettings.rateLimitLoginRequests,
|
||||||
skipSuccessfulRequests: true,
|
skipSuccessfulRequests: true,
|
||||||
onLimitReached: this.loginLimitReached
|
onLimitReached: this.loginLimitReached
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const SocketIO = require('socket.io')
|
const SocketIO = require('socket.io')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const Database = require('./Database')
|
||||||
|
|
||||||
class SocketAuthority {
|
class SocketAuthority {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -18,7 +19,7 @@ class SocketAuthority {
|
|||||||
onlineUsersMap[client.user.id].connections++
|
onlineUsersMap[client.user.id].connections++
|
||||||
} else {
|
} else {
|
||||||
onlineUsersMap[client.user.id] = {
|
onlineUsersMap[client.user.id] = {
|
||||||
...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems),
|
...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems),
|
||||||
connections: 1
|
connections: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +108,7 @@ class SocketAuthority {
|
|||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
} else {
|
} else {
|
||||||
Logger.debug('[Server] User Offline ' + _client.user.username)
|
Logger.debug('[Server] User Offline ' + _client.user.username)
|
||||||
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
|
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems))
|
||||||
|
|
||||||
const disconnectTime = Date.now() - _client.connected_at
|
const disconnectTime = Date.now() - _client.connected_at
|
||||||
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
|
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
|
||||||
@ -160,11 +161,11 @@ class SocketAuthority {
|
|||||||
|
|
||||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||||
|
|
||||||
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
|
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems))
|
||||||
|
|
||||||
// Update user lastSeen
|
// Update user lastSeen
|
||||||
user.lastSeen = Date.now()
|
user.lastSeen = Date.now()
|
||||||
await this.Server.db.updateEntity('user', user)
|
await Database.updateUser(user)
|
||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
userId: client.user.id,
|
userId: client.user.id,
|
||||||
@ -186,7 +187,7 @@ class SocketAuthority {
|
|||||||
|
|
||||||
if (client.user) {
|
if (client.user) {
|
||||||
Logger.debug('[Server] User Offline ' + client.user.username)
|
Logger.debug('[Server] User Offline ' + client.user.username)
|
||||||
this.adminEmitter('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems))
|
this.adminEmitter('user_offline', client.user.toJSONForPublic(null, Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.clients[socketId].user
|
delete this.clients[socketId].user
|
||||||
|
@ -4,6 +4,7 @@ const { createNewSortInstance } = require('../libs/fastSort')
|
|||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const { reqSupportsWebp } = require('../utils/index')
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class AuthorController {
|
|||||||
|
|
||||||
// Used on author landing page to include library items and items grouped in series
|
// Used on author landing page to include library items and items grouped in series
|
||||||
if (include.includes('items')) {
|
if (include.includes('items')) {
|
||||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
authorJson.libraryItems = Database.libraryItems.filter(li => {
|
||||||
if (libraryId && li.libraryId !== libraryId) return false
|
if (libraryId && li.libraryId !== libraryId) return false
|
||||||
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
||||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
@ -97,23 +98,29 @@ class AuthorController {
|
|||||||
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||||
|
|
||||||
// Check if author name matches another author and merge the authors
|
// Check if author name matches another author and merge the authors
|
||||||
const existingAuthor = authorNameUpdate ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||||
if (existingAuthor) {
|
if (existingAuthor) {
|
||||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
const bookAuthorsToCreate = []
|
||||||
|
const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||||
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||||
|
bookAuthorsToCreate.push({
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
authorId: existingAuthor.id
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if (itemsWithAuthor.length) {
|
if (itemsWithAuthor.length) {
|
||||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
await Database.removeBulkBookAuthors(req.author.id) // Remove all old BookAuthor
|
||||||
|
await Database.createBulkBookAuthors(bookAuthorsToCreate) // Create all new BookAuthor
|
||||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old author
|
// Remove old author
|
||||||
await this.db.removeEntity('author', req.author.id)
|
await Database.removeAuthor(req.author.id)
|
||||||
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
||||||
|
|
||||||
// Send updated num books for merged author
|
// Send updated num books for merged author
|
||||||
const numBooks = this.db.libraryItems.filter(li => {
|
const numBooks = Database.libraryItems.filter(li => {
|
||||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
|
||||||
}).length
|
}).length
|
||||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||||
@ -131,18 +138,17 @@ class AuthorController {
|
|||||||
req.author.updatedAt = Date.now()
|
req.author.updatedAt = Date.now()
|
||||||
|
|
||||||
if (authorNameUpdate) { // Update author name on all books
|
if (authorNameUpdate) { // Update author name on all books
|
||||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
itemsWithAuthor.forEach(libraryItem => {
|
itemsWithAuthor.forEach(libraryItem => {
|
||||||
libraryItem.media.metadata.updateAuthor(req.author)
|
libraryItem.media.metadata.updateAuthor(req.author)
|
||||||
})
|
})
|
||||||
if (itemsWithAuthor.length) {
|
if (itemsWithAuthor.length) {
|
||||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
|
||||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateEntity('author', req.author)
|
await Database.updateAuthor(req.author)
|
||||||
const numBooks = this.db.libraryItems.filter(li => {
|
const numBooks = Database.libraryItems.filter(li => {
|
||||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
}).length
|
}).length
|
||||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
@ -159,7 +165,7 @@ class AuthorController {
|
|||||||
var q = (req.query.q || '').toLowerCase()
|
var q = (req.query.q || '').toLowerCase()
|
||||||
if (!q) return res.json([])
|
if (!q) return res.json([])
|
||||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||||
var authors = this.db.authors.filter(au => au.name.toLowerCase().includes(q))
|
var authors = Database.authors.filter(au => au.name.toLowerCase().includes(q))
|
||||||
authors = authors.slice(0, limit)
|
authors = authors.slice(0, limit)
|
||||||
res.json({
|
res.json({
|
||||||
results: authors
|
results: authors
|
||||||
@ -204,8 +210,8 @@ class AuthorController {
|
|||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
req.author.updatedAt = Date.now()
|
req.author.updatedAt = Date.now()
|
||||||
|
|
||||||
await this.db.updateEntity('author', req.author)
|
await Database.updateAuthor(req.author)
|
||||||
const numBooks = this.db.libraryItems.filter(li => {
|
const numBooks = Database.libraryItems.filter(li => {
|
||||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
}).length
|
}).length
|
||||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
@ -238,7 +244,7 @@ class AuthorController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var author = this.db.authors.find(au => au.id === req.params.id)
|
var author = Database.authors.find(au => au.id === req.params.id)
|
||||||
if (!author) return res.sendStatus(404)
|
if (!author) return res.sendStatus(404)
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const Collection = require('../objects/Collection')
|
const Collection = require('../objects/Collection')
|
||||||
|
|
||||||
@ -13,22 +14,22 @@ class CollectionController {
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
return res.status(500).send('Invalid collection data')
|
return res.status(500).send('Invalid collection data')
|
||||||
}
|
}
|
||||||
var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems)
|
var jsonExpanded = newCollection.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.insertEntity('collection', newCollection)
|
await Database.createCollection(newCollection)
|
||||||
SocketAuthority.emitter('collection_added', jsonExpanded)
|
SocketAuthority.emitter('collection_added', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(req, res) {
|
findAll(req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
collections: this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems))
|
collections: Database.collections.map(c => c.toJSONExpanded(Database.libraryItems))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
const includeEntities = (req.query.include || '').split(',')
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
|
|
||||||
const collectionExpanded = req.collection.toJSONExpanded(this.db.libraryItems)
|
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
if (includeEntities.includes('rssfeed')) {
|
if (includeEntities.includes('rssfeed')) {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||||
@ -41,9 +42,9 @@ class CollectionController {
|
|||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
const wasUpdated = collection.update(req.body)
|
const wasUpdated = collection.update(req.body)
|
||||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('collection', collection)
|
await Database.updateCollection(collection)
|
||||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||||
}
|
}
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
@ -51,19 +52,19 @@ class CollectionController {
|
|||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
// Close rss feed - remove from db and emit socket event
|
// Close rss feed - remove from db and emit socket event
|
||||||
await this.rssFeedManager.closeFeedForEntityId(collection.id)
|
await this.rssFeedManager.closeFeedForEntityId(collection.id)
|
||||||
|
|
||||||
await this.db.removeEntity('collection', collection.id)
|
await Database.removeCollection(collection.id)
|
||||||
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBook(req, res) {
|
async addBook(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
const libraryItem = Database.libraryItems.find(li => li.id === req.body.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(500).send('Book not found')
|
return res.status(500).send('Book not found')
|
||||||
}
|
}
|
||||||
@ -74,8 +75,14 @@ class CollectionController {
|
|||||||
return res.status(500).send('Book already in collection')
|
return res.status(500).send('Book already in collection')
|
||||||
}
|
}
|
||||||
collection.addBook(req.body.id)
|
collection.addBook(req.body.id)
|
||||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.updateEntity('collection', collection)
|
|
||||||
|
const collectionBook = {
|
||||||
|
collectionId: collection.id,
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
order: collection.books.length
|
||||||
|
}
|
||||||
|
await Database.createCollectionBook(collectionBook)
|
||||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
@ -83,13 +90,18 @@ class CollectionController {
|
|||||||
// DELETE: api/collections/:id/book/:bookId
|
// DELETE: api/collections/:id/book/:bookId
|
||||||
async removeBook(req, res) {
|
async removeBook(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
|
const libraryItem = Database.libraryItems.find(li => li.id === req.params.bookId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
if (collection.books.includes(req.params.bookId)) {
|
if (collection.books.includes(req.params.bookId)) {
|
||||||
collection.removeBook(req.params.bookId)
|
collection.removeBook(req.params.bookId)
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.updateEntity('collection', collection)
|
|
||||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||||
|
await Database.updateCollection(collection)
|
||||||
}
|
}
|
||||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/collections/:id/batch/add
|
// POST: api/collections/:id/batch/add
|
||||||
@ -98,19 +110,30 @@ class CollectionController {
|
|||||||
if (!req.body.books || !req.body.books.length) {
|
if (!req.body.books || !req.body.books.length) {
|
||||||
return res.status(500).send('Invalid request body')
|
return res.status(500).send('Invalid request body')
|
||||||
}
|
}
|
||||||
var bookIdsToAdd = req.body.books
|
const bookIdsToAdd = req.body.books
|
||||||
var hasUpdated = false
|
const collectionBooksToAdd = []
|
||||||
for (let i = 0; i < bookIdsToAdd.length; i++) {
|
let hasUpdated = false
|
||||||
if (!collection.books.includes(bookIdsToAdd[i])) {
|
|
||||||
collection.addBook(bookIdsToAdd[i])
|
let order = collection.books.length
|
||||||
|
for (const libraryItemId of bookIdsToAdd) {
|
||||||
|
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||||
|
if (!libraryItem) continue
|
||||||
|
if (!collection.books.includes(libraryItemId)) {
|
||||||
|
collection.addBook(libraryItemId)
|
||||||
|
collectionBooksToAdd.push({
|
||||||
|
collectionId: collection.id,
|
||||||
|
bookId: libraryItem.media.id,
|
||||||
|
order: order++
|
||||||
|
})
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('collection', collection)
|
await Database.createBulkCollectionBooks(collectionBooksToAdd)
|
||||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/collections/:id/batch/remove
|
// POST: api/collections/:id/batch/remove
|
||||||
@ -120,23 +143,26 @@ class CollectionController {
|
|||||||
return res.status(500).send('Invalid request body')
|
return res.status(500).send('Invalid request body')
|
||||||
}
|
}
|
||||||
var bookIdsToRemove = req.body.books
|
var bookIdsToRemove = req.body.books
|
||||||
var hasUpdated = false
|
let hasUpdated = false
|
||||||
for (let i = 0; i < bookIdsToRemove.length; i++) {
|
for (const libraryItemId of bookIdsToRemove) {
|
||||||
if (collection.books.includes(bookIdsToRemove[i])) {
|
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||||
collection.removeBook(bookIdsToRemove[i])
|
if (!libraryItem) continue
|
||||||
|
|
||||||
|
if (collection.books.includes(libraryItemId)) {
|
||||||
|
collection.removeBook(libraryItemId)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('collection', collection)
|
await Database.updateCollection(collection)
|
||||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
const collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = Database.collections.find(c => c.id === req.params.id)
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return res.status(404).send('Collection not found')
|
return res.status(404).send('Collection not found')
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
class EmailController {
|
class EmailController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
getSettings(req, res) {
|
getSettings(req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
settings: this.db.emailSettings
|
settings: Database.emailSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSettings(req, res) {
|
async updateSettings(req, res) {
|
||||||
const updated = this.db.emailSettings.update(req.body)
|
const updated = Database.emailSettings.update(req.body)
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await this.db.updateEntity('settings', this.db.emailSettings)
|
await Database.updateSetting(Database.emailSettings)
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
settings: this.db.emailSettings
|
settings: Database.emailSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,24 +37,24 @@ class EmailController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = this.db.emailSettings.update({
|
const updated = Database.emailSettings.update({
|
||||||
ereaderDevices
|
ereaderDevices
|
||||||
})
|
})
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await this.db.updateEntity('settings', this.db.emailSettings)
|
await Database.updateSetting(Database.emailSettings)
|
||||||
SocketAuthority.adminEmitter('ereader-devices-updated', {
|
SocketAuthority.adminEmitter('ereader-devices-updated', {
|
||||||
ereaderDevices: this.db.emailSettings.ereaderDevices
|
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
ereaderDevices: this.db.emailSettings.ereaderDevices
|
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEBookToDevice(req, res) {
|
async sendEBookToDevice(req, res) {
|
||||||
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
||||||
|
|
||||||
const libraryItem = this.db.getLibraryItem(req.body.libraryItemId)
|
const libraryItem = Database.getLibraryItem(req.body.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Library item not found')
|
return res.status(404).send('Library item not found')
|
||||||
}
|
}
|
||||||
@ -67,7 +68,7 @@ class EmailController {
|
|||||||
return res.status(404).send('EBook file not found')
|
return res.status(404).send('EBook file not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const device = this.db.emailSettings.getEReaderDevice(req.body.deviceName)
|
const device = Database.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return res.status(404).send('E-reader device not found')
|
return res.status(404).send('E-reader device not found')
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
class FileSystemController {
|
class FileSystemController {
|
||||||
@ -16,7 +17,7 @@ class FileSystemController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Do not include existing mapped library paths in response
|
// Do not include existing mapped library paths in response
|
||||||
this.db.libraries.forEach(lib => {
|
Database.libraries.forEach(lib => {
|
||||||
lib.folders.forEach((folder) => {
|
lib.folders.forEach((folder) => {
|
||||||
let dir = folder.fullPath
|
let dir = folder.fullPath
|
||||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||||
|
@ -9,6 +9,9 @@ const { sort, createNewSortInstance } = require('../libs/fastSort')
|
|||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
class LibraryController {
|
class LibraryController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -40,13 +43,13 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const library = new Library()
|
const library = new Library()
|
||||||
newLibraryPayload.displayOrder = this.db.libraries.length + 1
|
newLibraryPayload.displayOrder = Database.libraries.length + 1
|
||||||
library.setData(newLibraryPayload)
|
library.setData(newLibraryPayload)
|
||||||
await this.db.insertEntity('library', library)
|
await Database.createLibrary(library)
|
||||||
|
|
||||||
// Only emit to users with access to library
|
// Only emit to users with access to library
|
||||||
const userFilter = (user) => {
|
const userFilter = (user) => {
|
||||||
return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id)
|
return user.checkCanAccessLibrary?.(library.id)
|
||||||
}
|
}
|
||||||
SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
|
SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
|
||||||
|
|
||||||
@ -58,14 +61,15 @@ class LibraryController {
|
|||||||
|
|
||||||
findAll(req, res) {
|
findAll(req, res) {
|
||||||
const librariesAccessible = req.user.librariesAccessible || []
|
const librariesAccessible = req.user.librariesAccessible || []
|
||||||
if (librariesAccessible && librariesAccessible.length) {
|
if (librariesAccessible.length) {
|
||||||
return res.json({
|
return res.json({
|
||||||
libraries: this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
libraries: Database.libraries.map(lib => lib.toJSON())
|
||||||
|
// libraries: Database.libraries.map(lib => lib.toJSON())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +79,7 @@ class LibraryController {
|
|||||||
return res.json({
|
return res.json({
|
||||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||||
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
||||||
numUserPlaylists: this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
|
numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
|
||||||
library: req.library
|
library: req.library
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -128,14 +132,14 @@ class LibraryController {
|
|||||||
this.cronManager.updateLibraryScanCron(library)
|
this.cronManager.updateLibraryScanCron(library)
|
||||||
|
|
||||||
// Remove libraryItems no longer in library
|
// Remove libraryItems no longer in library
|
||||||
const itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
const itemsToRemove = Database.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||||
if (itemsToRemove.length) {
|
if (itemsToRemove.length) {
|
||||||
Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`)
|
Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`)
|
||||||
for (let i = 0; i < itemsToRemove.length; i++) {
|
for (let i = 0; i < itemsToRemove.length; i++) {
|
||||||
await this.handleDeleteLibraryItem(itemsToRemove[i])
|
await this.handleDeleteLibraryItem(itemsToRemove[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('library', library)
|
await Database.updateLibrary(library)
|
||||||
|
|
||||||
// Only emit to users with access to library
|
// Only emit to users with access to library
|
||||||
const userFilter = (user) => {
|
const userFilter = (user) => {
|
||||||
@ -153,21 +157,21 @@ class LibraryController {
|
|||||||
this.watcher.removeLibrary(library)
|
this.watcher.removeLibrary(library)
|
||||||
|
|
||||||
// Remove collections for library
|
// Remove collections for library
|
||||||
const collections = this.db.collections.filter(c => c.libraryId === library.id)
|
const collections = Database.collections.filter(c => c.libraryId === library.id)
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
|
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
|
||||||
await this.db.removeEntity('collection', collection.id)
|
await Database.removeCollection(collection.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove items in this library
|
// Remove items in this library
|
||||||
const libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
const libraryItems = Database.libraryItems.filter(li => li.libraryId === library.id)
|
||||||
Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`)
|
Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`)
|
||||||
for (let i = 0; i < libraryItems.length; i++) {
|
for (let i = 0; i < libraryItems.length; i++) {
|
||||||
await this.handleDeleteLibraryItem(libraryItems[i])
|
await this.handleDeleteLibraryItem(libraryItems[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryJson = library.toJSON()
|
const libraryJson = library.toJSON()
|
||||||
await this.db.removeEntity('library', library.id)
|
await Database.removeLibrary(library.id)
|
||||||
SocketAuthority.emitter('library_removed', libraryJson)
|
SocketAuthority.emitter('library_removed', libraryJson)
|
||||||
return res.json(libraryJson)
|
return res.json(libraryJson)
|
||||||
}
|
}
|
||||||
@ -209,7 +213,7 @@ class LibraryController {
|
|||||||
// If also filtering by series, will not collapse the filtered series as this would lead
|
// If also filtering by series, will not collapse the filtered series as this would lead
|
||||||
// to series having a collapsed series that is just that series.
|
// to series having a collapsed series that is just that series.
|
||||||
if (payload.collapseseries) {
|
if (payload.collapseseries) {
|
||||||
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries, req.library.settings.hideSingleBookSeries)
|
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, Database.series, filterSeries, req.library.settings.hideSingleBookSeries)
|
||||||
|
|
||||||
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
||||||
libraryItems = collapsedItems
|
libraryItems = collapsedItems
|
||||||
@ -237,7 +241,7 @@ class LibraryController {
|
|||||||
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||||
sortArray.push({
|
sortArray.push({
|
||||||
asc: (li) => {
|
asc: (li) => {
|
||||||
if (this.db.serverSettings.sortingIgnorePrefix) {
|
if (Database.serverSettings.sortingIgnorePrefix) {
|
||||||
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
||||||
} else {
|
} else {
|
||||||
return li.collapsedSeries?.name || li.media.metadata.title
|
return li.collapsedSeries?.name || li.media.metadata.title
|
||||||
@ -255,7 +259,7 @@ class LibraryController {
|
|||||||
|
|
||||||
// Handle server setting sortingIgnorePrefix
|
// Handle server setting sortingIgnorePrefix
|
||||||
const sortByTitle = sortKey === 'media.metadata.title'
|
const sortByTitle = sortKey === 'media.metadata.title'
|
||||||
if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) {
|
if (sortByTitle && Database.serverSettings.sortingIgnorePrefix) {
|
||||||
// BookMetadata.js has titleIgnorePrefix getter
|
// BookMetadata.js has titleIgnorePrefix getter
|
||||||
sortKey += 'IgnorePrefix'
|
sortKey += 'IgnorePrefix'
|
||||||
}
|
}
|
||||||
@ -267,7 +271,7 @@ class LibraryController {
|
|||||||
sortArray.push({
|
sortArray.push({
|
||||||
asc: (li) => {
|
asc: (li) => {
|
||||||
if (li.collapsedSeries) {
|
if (li.collapsedSeries) {
|
||||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
return Database.serverSettings.sortingIgnorePrefix ?
|
||||||
li.collapsedSeries.nameIgnorePrefix :
|
li.collapsedSeries.nameIgnorePrefix :
|
||||||
li.collapsedSeries.name
|
li.collapsedSeries.name
|
||||||
} else {
|
} else {
|
||||||
@ -284,7 +288,7 @@ class LibraryController {
|
|||||||
if (mediaIsBook && sortBySequence) {
|
if (mediaIsBook && sortBySequence) {
|
||||||
return li.media.metadata.getSeries(filterSeries).sequence
|
return li.media.metadata.getSeries(filterSeries).sequence
|
||||||
} else if (mediaIsBook && sortByTitle && li.collapsedSeries) {
|
} else if (mediaIsBook && sortByTitle && li.collapsedSeries) {
|
||||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
return Database.serverSettings.sortingIgnorePrefix ?
|
||||||
li.collapsedSeries.nameIgnorePrefix :
|
li.collapsedSeries.nameIgnorePrefix :
|
||||||
li.collapsedSeries.name
|
li.collapsedSeries.name
|
||||||
} else {
|
} else {
|
||||||
@ -405,7 +409,7 @@ class LibraryController {
|
|||||||
include: include.join(',')
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries)
|
let series = libraryHelpers.getSeriesFromBooks(libraryItems, Database.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries)
|
||||||
|
|
||||||
const direction = payload.sortDesc ? 'desc' : 'asc'
|
const direction = payload.sortDesc ? 'desc' : 'asc'
|
||||||
series = naturalSort(series).by([
|
series = naturalSort(series).by([
|
||||||
@ -422,7 +426,7 @@ class LibraryController {
|
|||||||
} else if (payload.sortBy === 'lastBookAdded') {
|
} else if (payload.sortBy === 'lastBookAdded') {
|
||||||
return Math.max(...(se.books).map(x => x.addedAt), 0)
|
return Math.max(...(se.books).map(x => x.addedAt), 0)
|
||||||
} else { // sort by name
|
} else { // sort by name
|
||||||
return this.db.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,7 +470,7 @@ class LibraryController {
|
|||||||
include: include.join(',')
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
let collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
let collections = Database.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
||||||
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||||
|
|
||||||
// If all books restricted to user in this collection then hide this collection
|
// If all books restricted to user in this collection then hide this collection
|
||||||
@ -493,7 +497,7 @@ class LibraryController {
|
|||||||
|
|
||||||
// api/libraries/:id/playlists
|
// api/libraries/:id/playlists
|
||||||
async getUserPlaylistsForLibrary(req, res) {
|
async getUserPlaylistsForLibrary(req, res) {
|
||||||
let playlistsForUser = this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems))
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
@ -517,7 +521,7 @@ class LibraryController {
|
|||||||
return res.status(400).send('Invalid library media type')
|
return res.status(400).send('Invalid library media type')
|
||||||
}
|
}
|
||||||
|
|
||||||
let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id)
|
let libraryItems = Database.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||||
let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems)
|
let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems)
|
||||||
albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
|
albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
|
||||||
|
|
||||||
@ -561,26 +565,26 @@ class LibraryController {
|
|||||||
var orderdata = req.body
|
var orderdata = req.body
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
for (let i = 0; i < orderdata.length; i++) {
|
for (let i = 0; i < orderdata.length; i++) {
|
||||||
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
var library = Database.libraries.find(lib => lib.id === orderdata[i].id)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
await this.db.updateEntity('library', library)
|
await Database.updateLibrary(library)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
this.db.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
Logger.debug(`[LibraryController] Updated library display orders`)
|
Logger.debug(`[LibraryController] Updated library display orders`)
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[LibraryController] Library orders were up to date`)
|
Logger.debug(`[LibraryController] Library orders were up to date`)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
libraries: Database.libraries.map(lib => lib.toJSON())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,7 +614,7 @@ class LibraryController {
|
|||||||
if (queryResult.series?.length) {
|
if (queryResult.series?.length) {
|
||||||
queryResult.series.forEach((se) => {
|
queryResult.series.forEach((se) => {
|
||||||
if (!seriesMatches[se.id]) {
|
if (!seriesMatches[se.id]) {
|
||||||
const _series = this.db.series.find(_se => _se.id === se.id)
|
const _series = Database.series.find(_se => _se.id === se.id)
|
||||||
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
||||||
} else {
|
} else {
|
||||||
seriesMatches[se.id].books.push(li.toJSON())
|
seriesMatches[se.id].books.push(li.toJSON())
|
||||||
@ -620,7 +624,7 @@ class LibraryController {
|
|||||||
if (queryResult.authors?.length) {
|
if (queryResult.authors?.length) {
|
||||||
queryResult.authors.forEach((au) => {
|
queryResult.authors.forEach((au) => {
|
||||||
if (!authorMatches[au.id]) {
|
if (!authorMatches[au.id]) {
|
||||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||||
if (_author) {
|
if (_author) {
|
||||||
authorMatches[au.id] = _author.toJSON()
|
authorMatches[au.id] = _author.toJSON()
|
||||||
authorMatches[au.id].numBooks = 1
|
authorMatches[au.id].numBooks = 1
|
||||||
@ -687,7 +691,7 @@ class LibraryController {
|
|||||||
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
||||||
li.media.metadata.authors.forEach((au) => {
|
li.media.metadata.authors.forEach((au) => {
|
||||||
if (!authors[au.id]) {
|
if (!authors[au.id]) {
|
||||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||||
if (_author) {
|
if (_author) {
|
||||||
authors[au.id] = _author.toJSON()
|
authors[au.id] = _author.toJSON()
|
||||||
authors[au.id].numBooks = 1
|
authors[au.id].numBooks = 1
|
||||||
@ -749,7 +753,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (itemsUpdated.length) {
|
if (itemsUpdated.length) {
|
||||||
await this.db.updateLibraryItems(itemsUpdated)
|
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,7 +778,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (itemsUpdated.length) {
|
if (itemsUpdated.length) {
|
||||||
await this.db.updateLibraryItems(itemsUpdated)
|
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,12 +862,12 @@ class LibraryController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
const library = this.db.libraries.find(lib => lib.id === req.params.id)
|
const library = Database.libraries.find(lib => lib.id === req.params.id)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
}
|
}
|
||||||
req.library = library
|
req.library = library
|
||||||
req.libraryItems = this.db.libraryItems.filter(li => {
|
req.libraryItems = Database.libraryItems.filter(li => {
|
||||||
return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li)
|
return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li)
|
||||||
})
|
})
|
||||||
next()
|
next()
|
||||||
|
@ -2,9 +2,10 @@ const Path = require('path')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const zipHelpers = require('../utils/zipHelpers')
|
const zipHelpers = require('../utils/zipHelpers')
|
||||||
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
const { ScanResult } = require('../utils/constants')
|
const { ScanResult } = require('../utils/constants')
|
||||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ class LibraryItemController {
|
|||||||
if (item.mediaType == 'book') {
|
if (item.mediaType == 'book') {
|
||||||
if (includeEntities.includes('authors')) {
|
if (includeEntities.includes('authors')) {
|
||||||
item.media.metadata.authors = item.media.metadata.authors.map(au => {
|
item.media.metadata.authors = item.media.metadata.authors.map(au => {
|
||||||
var author = this.db.authors.find(_au => _au.id === au.id)
|
var author = Database.authors.find(_au => _au.id === au.id)
|
||||||
if (!author) return null
|
if (!author) return null
|
||||||
return {
|
return {
|
||||||
...author
|
...author
|
||||||
@ -61,7 +62,7 @@ class LibraryItemController {
|
|||||||
const hasUpdates = libraryItem.update(req.body)
|
const hasUpdates = libraryItem.update(req.body)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json(libraryItem.toJSON())
|
res.json(libraryItem.toJSON())
|
||||||
@ -139,7 +140,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
@ -174,7 +175,7 @@ class LibraryItemController {
|
|||||||
return res.status(500).send('Unknown error occurred')
|
return res.status(500).send('Unknown error occurred')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -194,7 +195,7 @@ class LibraryItemController {
|
|||||||
return res.status(500).send(validationResult.error)
|
return res.status(500).send(validationResult.error)
|
||||||
}
|
}
|
||||||
if (validationResult.updated) {
|
if (validationResult.updated) {
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
@ -210,7 +211,7 @@ class LibraryItemController {
|
|||||||
if (libraryItem.media.coverPath) {
|
if (libraryItem.media.coverPath) {
|
||||||
libraryItem.updateMediaCover('')
|
libraryItem.updateMediaCover('')
|
||||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +283,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
libraryItem.media.updateAudioTracks(orderedFileData)
|
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
res.json(libraryItem.toJSON())
|
res.json(libraryItem.toJSON())
|
||||||
}
|
}
|
||||||
@ -309,7 +310,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
const itemsToDelete = Database.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||||
if (!itemsToDelete.length) {
|
if (!itemsToDelete.length) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
@ -338,7 +339,7 @@ class LibraryItemController {
|
|||||||
|
|
||||||
for (let i = 0; i < updatePayloads.length; i++) {
|
for (let i = 0; i < updatePayloads.length; i++) {
|
||||||
var mediaPayload = updatePayloads[i].mediaPayload
|
var mediaPayload = updatePayloads[i].mediaPayload
|
||||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
var libraryItem = Database.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||||
if (!libraryItem) return null
|
if (!libraryItem) return null
|
||||||
|
|
||||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||||
@ -346,7 +347,7 @@ class LibraryItemController {
|
|||||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
itemsUpdated++
|
itemsUpdated++
|
||||||
}
|
}
|
||||||
@ -366,7 +367,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
const libraryItems = []
|
const libraryItems = []
|
||||||
libraryItemIds.forEach((lid) => {
|
libraryItemIds.forEach((lid) => {
|
||||||
const li = this.db.libraryItems.find(_li => _li.id === lid)
|
const li = Database.libraryItems.find(_li => _li.id === lid)
|
||||||
if (li) libraryItems.push(li.toJSONExpanded())
|
if (li) libraryItems.push(li.toJSONExpanded())
|
||||||
})
|
})
|
||||||
res.json({
|
res.json({
|
||||||
@ -389,7 +390,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||||
if (!libraryItems?.length) {
|
if (!libraryItems?.length) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
@ -424,7 +425,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||||
if (!libraryItems?.length) {
|
if (!libraryItems?.length) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
@ -441,15 +442,17 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/items/all
|
// DELETE: api/items/all
|
||||||
|
// TODO: Remove
|
||||||
async deleteAll(req, res) {
|
async deleteAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
return res.sendStatus(404)
|
||||||
Logger.warn('User other than admin attempted to delete all library items', req.user)
|
// if (!req.user.isAdminOrUp) {
|
||||||
return res.sendStatus(403)
|
// Logger.warn('User other than admin attempted to delete all library items', req.user)
|
||||||
}
|
// return res.sendStatus(403)
|
||||||
Logger.info('Removing all Library Items')
|
// }
|
||||||
var success = await this.db.recreateLibraryItemsDb()
|
// Logger.info('Removing all Library Items')
|
||||||
if (success) res.sendStatus(200)
|
// var success = await this.db.recreateLibraryItemsDb()
|
||||||
else res.sendStatus(500)
|
// if (success) res.sendStatus(200)
|
||||||
|
// else res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/items/:id/scan (admin)
|
// POST: api/items/:id/scan (admin)
|
||||||
@ -504,7 +507,7 @@ class LibraryItemController {
|
|||||||
const chapters = req.body.chapters || []
|
const chapters = req.body.chapters || []
|
||||||
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateLibraryItem(req.libraryItem)
|
await Database.updateLibraryItem(req.libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +589,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.libraryItem.updatedAt = Date.now()
|
req.libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(req.libraryItem)
|
await Database.updateLibraryItem(req.libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -682,13 +685,13 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.libraryItem.updatedAt = Date.now()
|
req.libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(req.libraryItem)
|
await Database.updateLibraryItem(req.libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
req.libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
const { sort } = require('../libs/fastSort')
|
const { sort } = require('../libs/fastSort')
|
||||||
const { isObject, toNumber } = require('../utils/index')
|
const { toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class MeController {
|
class MeController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -33,7 +34,7 @@ class MeController {
|
|||||||
|
|
||||||
// GET: api/me/listening-stats
|
// GET: api/me/listening-stats
|
||||||
async getListeningStats(req, res) {
|
async getListeningStats(req, res) {
|
||||||
var listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||||
res.json(listeningStats)
|
res.json(listeningStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,21 +52,21 @@ class MeController {
|
|||||||
if (!req.user.removeMediaProgress(req.params.id)) {
|
if (!req.user.removeMediaProgress(req.params.id)) {
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.removeMediaProgress(req.params.id)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/me/progress/:id
|
// PATCH: api/me/progress/:id
|
||||||
async createUpdateMediaProgress(req, res) {
|
async createUpdateMediaProgress(req, res) {
|
||||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Item not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
|
if (req.user.createUpdateMediaProgress(libraryItem, req.body)) {
|
||||||
if (wasUpdated) {
|
const mediaProgress = req.user.getMediaProgress(libraryItem.id)
|
||||||
await this.db.updateEntity('user', req.user)
|
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@ -73,8 +74,8 @@ class MeController {
|
|||||||
|
|
||||||
// PATCH: api/me/progress/:id/:episodeId
|
// PATCH: api/me/progress/:id/:episodeId
|
||||||
async createUpdateEpisodeMediaProgress(req, res) {
|
async createUpdateEpisodeMediaProgress(req, res) {
|
||||||
var episodeId = req.params.episodeId
|
const episodeId = req.params.episodeId
|
||||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Item not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
@ -83,9 +84,9 @@ class MeController {
|
|||||||
return res.status(404).send('Episode not found')
|
return res.status(404).send('Episode not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)
|
if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) {
|
||||||
if (wasUpdated) {
|
const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId)
|
||||||
await this.db.updateEntity('user', req.user)
|
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@ -93,24 +94,26 @@ class MeController {
|
|||||||
|
|
||||||
// PATCH: api/me/progress/batch/update
|
// PATCH: api/me/progress/batch/update
|
||||||
async batchUpdateMediaProgress(req, res) {
|
async batchUpdateMediaProgress(req, res) {
|
||||||
var itemProgressPayloads = req.body
|
const itemProgressPayloads = req.body
|
||||||
if (!itemProgressPayloads || !itemProgressPayloads.length) {
|
if (!itemProgressPayloads?.length) {
|
||||||
return res.status(400).send('Missing request payload')
|
return res.status(400).send('Missing request payload')
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldUpdate = false
|
let shouldUpdate = false
|
||||||
itemProgressPayloads.forEach((itemProgress) => {
|
for (const itemProgress of itemProgressPayloads) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)
|
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
|
||||||
if (wasUpdated) shouldUpdate = true
|
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
|
||||||
|
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
await this.db.updateEntity('user', req.user)
|
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,18 +122,18 @@ class MeController {
|
|||||||
|
|
||||||
// POST: api/me/item/:id/bookmark
|
// POST: api/me/item/:id/bookmark
|
||||||
async createBookmark(req, res) {
|
async createBookmark(req, res) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!libraryItem) return res.sendStatus(404)
|
if (!libraryItem) return res.sendStatus(404)
|
||||||
const { time, title } = req.body
|
const { time, title } = req.body
|
||||||
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
|
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
res.json(bookmark)
|
res.json(bookmark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/me/item/:id/bookmark
|
// PATCH: api/me/item/:id/bookmark
|
||||||
async updateBookmark(req, res) {
|
async updateBookmark(req, res) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!libraryItem) return res.sendStatus(404)
|
if (!libraryItem) return res.sendStatus(404)
|
||||||
const { time, title } = req.body
|
const { time, title } = req.body
|
||||||
if (!req.user.findBookmark(libraryItem.id, time)) {
|
if (!req.user.findBookmark(libraryItem.id, time)) {
|
||||||
@ -139,14 +142,14 @@ class MeController {
|
|||||||
}
|
}
|
||||||
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
|
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
|
||||||
if (!bookmark) return res.sendStatus(500)
|
if (!bookmark) return res.sendStatus(500)
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
res.json(bookmark)
|
res.json(bookmark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/me/item/:id/bookmark/:time
|
// DELETE: api/me/item/:id/bookmark/:time
|
||||||
async removeBookmark(req, res) {
|
async removeBookmark(req, res) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!libraryItem) return res.sendStatus(404)
|
if (!libraryItem) return res.sendStatus(404)
|
||||||
var time = Number(req.params.time)
|
var time = Number(req.params.time)
|
||||||
if (isNaN(time)) return res.sendStatus(500)
|
if (isNaN(time)) return res.sendStatus(500)
|
||||||
@ -156,7 +159,7 @@ class MeController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
req.user.removeBookmark(libraryItem.id, time)
|
req.user.removeBookmark(libraryItem.id, time)
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -178,16 +181,16 @@ class MeController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
const updatedLocalMediaProgress = []
|
const updatedLocalMediaProgress = []
|
||||||
var numServerProgressUpdates = 0
|
let numServerProgressUpdates = 0
|
||||||
const updatedServerMediaProgress = []
|
const updatedServerMediaProgress = []
|
||||||
const localMediaProgress = req.body.localMediaProgress || []
|
const localMediaProgress = req.body.localMediaProgress || []
|
||||||
|
|
||||||
localMediaProgress.forEach(localProgress => {
|
for (const localProgress of localMediaProgress) {
|
||||||
if (!localProgress.libraryItemId) {
|
if (!localProgress.libraryItemId) {
|
||||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId)
|
const libraryItem = Database.getLibraryItem(localProgress.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
||||||
return
|
return
|
||||||
@ -199,12 +202,14 @@ class MeController {
|
|||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||||
updatedServerMediaProgress.push(mediaProgress)
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||||
updatedServerMediaProgress.push(mediaProgress)
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
||||||
@ -222,11 +227,10 @@ class MeController {
|
|||||||
} else {
|
} else {
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
|
||||||
if (numServerProgressUpdates > 0) {
|
if (numServerProgressUpdates > 0) {
|
||||||
await this.db.updateEntity('user', req.user)
|
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +248,7 @@ class MeController {
|
|||||||
let itemsInProgress = []
|
let itemsInProgress = []
|
||||||
for (const mediaProgress of req.user.mediaProgress) {
|
for (const mediaProgress of req.user.mediaProgress) {
|
||||||
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||||
const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId)
|
const libraryItem = Database.getLibraryItem(mediaProgress.libraryItemId)
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||||
@ -274,7 +278,7 @@ class MeController {
|
|||||||
|
|
||||||
// GET: api/me/series/:id/remove-from-continue-listening
|
// GET: api/me/series/:id/remove-from-continue-listening
|
||||||
async removeSeriesFromContinueListening(req, res) {
|
async removeSeriesFromContinueListening(req, res) {
|
||||||
const series = this.db.series.find(se => se.id === req.params.id)
|
const series = Database.series.find(se => se.id === req.params.id)
|
||||||
if (!series) {
|
if (!series) {
|
||||||
Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`)
|
Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@ -282,7 +286,7 @@ class MeController {
|
|||||||
|
|
||||||
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
|
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.json(req.user.toJSONForBrowser())
|
res.json(req.user.toJSONForBrowser())
|
||||||
@ -290,7 +294,7 @@ class MeController {
|
|||||||
|
|
||||||
// GET: api/me/series/:id/readd-to-continue-listening
|
// GET: api/me/series/:id/readd-to-continue-listening
|
||||||
async readdSeriesFromContinueListening(req, res) {
|
async readdSeriesFromContinueListening(req, res) {
|
||||||
const series = this.db.series.find(se => se.id === req.params.id)
|
const series = Database.series.find(se => se.id === req.params.id)
|
||||||
if (!series) {
|
if (!series) {
|
||||||
Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`)
|
Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@ -298,7 +302,7 @@ class MeController {
|
|||||||
|
|
||||||
const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id)
|
const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.json(req.user.toJSONForBrowser())
|
res.json(req.user.toJSONForBrowser())
|
||||||
@ -308,7 +312,7 @@ class MeController {
|
|||||||
async removeItemFromContinueListening(req, res) {
|
async removeItemFromContinueListening(req, res) {
|
||||||
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('user', req.user)
|
await Database.updateUser(req.user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.json(req.user.toJSONForBrowser())
|
res.json(req.user.toJSONForBrowser())
|
||||||
|
@ -2,6 +2,7 @@ const Path = require('path')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
@ -30,7 +31,7 @@ class MiscController {
|
|||||||
var libraryId = req.body.library
|
var libraryId = req.body.library
|
||||||
var folderId = req.body.folder
|
var folderId = req.body.folder
|
||||||
|
|
||||||
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
var library = Database.libraries.find(lib => lib.id === libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return res.status(404).send(`Library not found with id ${libraryId}`)
|
return res.status(404).send(`Library not found with id ${libraryId}`)
|
||||||
}
|
}
|
||||||
@ -116,18 +117,18 @@ class MiscController {
|
|||||||
return res.status(500).send('Invalid settings update object')
|
return res.status(500).send('Invalid settings update object')
|
||||||
}
|
}
|
||||||
|
|
||||||
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
var madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||||
if (madeUpdates) {
|
if (madeUpdates) {
|
||||||
// If backup schedule is updated - update backup manager
|
// If backup schedule is updated - update backup manager
|
||||||
if (settingsUpdate.backupSchedule !== undefined) {
|
if (settingsUpdate.backupSchedule !== undefined) {
|
||||||
this.backupManager.updateCronSchedule()
|
this.backupManager.updateCronSchedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateServerSettings()
|
await Database.updateServerSettings()
|
||||||
}
|
}
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
serverSettings: this.db.serverSettings.toJSONForBrowser()
|
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +148,7 @@ class MiscController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
const tags = []
|
const tags = []
|
||||||
this.db.libraryItems.forEach((li) => {
|
Database.libraryItems.forEach((li) => {
|
||||||
if (li.media.tags && li.media.tags.length) {
|
if (li.media.tags && li.media.tags.length) {
|
||||||
li.media.tags.forEach((tag) => {
|
li.media.tags.forEach((tag) => {
|
||||||
if (!tags.includes(tag)) tags.push(tag)
|
if (!tags.includes(tag)) tags.push(tag)
|
||||||
@ -176,7 +177,7 @@ class MiscController {
|
|||||||
let tagMerged = false
|
let tagMerged = false
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
for (const li of this.db.libraryItems) {
|
for (const li of Database.libraryItems) {
|
||||||
if (!li.media.tags || !li.media.tags.length) continue
|
if (!li.media.tags || !li.media.tags.length) continue
|
||||||
|
|
||||||
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
||||||
@ -187,7 +188,7 @@ class MiscController {
|
|||||||
li.media.tags.push(newTag) // Add new tag
|
li.media.tags.push(newTag) // Add new tag
|
||||||
}
|
}
|
||||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||||
await this.db.updateLibraryItem(li)
|
await Database.updateLibraryItem(li)
|
||||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
numItemsUpdated++
|
numItemsUpdated++
|
||||||
}
|
}
|
||||||
@ -209,13 +210,13 @@ class MiscController {
|
|||||||
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
||||||
|
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
for (const li of this.db.libraryItems) {
|
for (const li of Database.libraryItems) {
|
||||||
if (!li.media.tags || !li.media.tags.length) continue
|
if (!li.media.tags || !li.media.tags.length) continue
|
||||||
|
|
||||||
if (li.media.tags.includes(tag)) {
|
if (li.media.tags.includes(tag)) {
|
||||||
li.media.tags = li.media.tags.filter(t => t !== tag)
|
li.media.tags = li.media.tags.filter(t => t !== tag)
|
||||||
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`)
|
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`)
|
||||||
await this.db.updateLibraryItem(li)
|
await Database.updateLibraryItem(li)
|
||||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
numItemsUpdated++
|
numItemsUpdated++
|
||||||
}
|
}
|
||||||
@ -233,7 +234,7 @@ class MiscController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
const genres = []
|
const genres = []
|
||||||
this.db.libraryItems.forEach((li) => {
|
Database.libraryItems.forEach((li) => {
|
||||||
if (li.media.metadata.genres && li.media.metadata.genres.length) {
|
if (li.media.metadata.genres && li.media.metadata.genres.length) {
|
||||||
li.media.metadata.genres.forEach((genre) => {
|
li.media.metadata.genres.forEach((genre) => {
|
||||||
if (!genres.includes(genre)) genres.push(genre)
|
if (!genres.includes(genre)) genres.push(genre)
|
||||||
@ -262,7 +263,7 @@ class MiscController {
|
|||||||
let genreMerged = false
|
let genreMerged = false
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
for (const li of this.db.libraryItems) {
|
for (const li of Database.libraryItems) {
|
||||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||||
|
|
||||||
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
||||||
@ -273,7 +274,7 @@ class MiscController {
|
|||||||
li.media.metadata.genres.push(newGenre) // Add new genre
|
li.media.metadata.genres.push(newGenre) // Add new genre
|
||||||
}
|
}
|
||||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||||
await this.db.updateLibraryItem(li)
|
await Database.updateLibraryItem(li)
|
||||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
numItemsUpdated++
|
numItemsUpdated++
|
||||||
}
|
}
|
||||||
@ -295,13 +296,13 @@ class MiscController {
|
|||||||
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||||
|
|
||||||
let numItemsUpdated = 0
|
let numItemsUpdated = 0
|
||||||
for (const li of this.db.libraryItems) {
|
for (const li of Database.libraryItems) {
|
||||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||||
|
|
||||||
if (li.media.metadata.genres.includes(genre)) {
|
if (li.media.metadata.genres.includes(genre)) {
|
||||||
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre)
|
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre)
|
||||||
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`)
|
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`)
|
||||||
await this.db.updateLibraryItem(li)
|
await Database.updateLibraryItem(li)
|
||||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
numItemsUpdated++
|
numItemsUpdated++
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
|
|
||||||
class NotificationController {
|
class NotificationController {
|
||||||
@ -7,14 +8,14 @@ class NotificationController {
|
|||||||
get(req, res) {
|
get(req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
data: this.notificationManager.getData(),
|
data: this.notificationManager.getData(),
|
||||||
settings: this.db.notificationSettings
|
settings: Database.notificationSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const updated = this.db.notificationSettings.update(req.body)
|
const updated = Database.notificationSettings.update(req.body)
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
await Database.updateSetting(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -29,31 +30,31 @@ class NotificationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNotification(req, res) {
|
async createNotification(req, res) {
|
||||||
const success = this.db.notificationSettings.createNotification(req.body)
|
const success = Database.notificationSettings.createNotification(req.body)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
await Database.updateSetting(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
res.json(this.db.notificationSettings)
|
res.json(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteNotification(req, res) {
|
async deleteNotification(req, res) {
|
||||||
if (this.db.notificationSettings.removeNotification(req.notification.id)) {
|
if (Database.notificationSettings.removeNotification(req.notification.id)) {
|
||||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
await Database.updateSetting(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
res.json(this.db.notificationSettings)
|
res.json(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNotification(req, res) {
|
async updateNotification(req, res) {
|
||||||
const success = this.db.notificationSettings.updateNotification(req.body)
|
const success = Database.notificationSettings.updateNotification(req.body)
|
||||||
if (success) {
|
if (success) {
|
||||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
await Database.updateSetting(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
res.json(this.db.notificationSettings)
|
res.json(Database.notificationSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendNotificationTest(req, res) {
|
async sendNotificationTest(req, res) {
|
||||||
if (!this.db.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
||||||
|
|
||||||
const success = await this.notificationManager.sendTestNotification(req.notification)
|
const success = await this.notificationManager.sendTestNotification(req.notification)
|
||||||
if (success) res.sendStatus(200)
|
if (success) res.sendStatus(200)
|
||||||
@ -66,7 +67,7 @@ class NotificationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
const notification = this.db.notificationSettings.getNotification(req.params.id)
|
const notification = Database.notificationSettings.getNotification(req.params.id)
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const Playlist = require('../objects/Playlist')
|
const Playlist = require('../objects/Playlist')
|
||||||
|
|
||||||
@ -14,8 +15,8 @@ class PlaylistController {
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
return res.status(400).send('Invalid playlist request data')
|
return res.status(400).send('Invalid playlist request data')
|
||||||
}
|
}
|
||||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.insertEntity('playlist', newPlaylist)
|
await Database.createPlaylist(newPlaylist)
|
||||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
@ -23,22 +24,22 @@ class PlaylistController {
|
|||||||
// GET: api/playlists
|
// GET: api/playlists
|
||||||
findAllForUser(req, res) {
|
findAllForUser(req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
playlists: this.db.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/playlists/:id
|
// GET: api/playlists/:id
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
res.json(req.playlist.toJSONExpanded(this.db.libraryItems))
|
res.json(req.playlist.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/playlists/:id
|
// PATCH: api/playlists/:id
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const playlist = req.playlist
|
const playlist = req.playlist
|
||||||
let wasUpdated = playlist.update(req.body)
|
let wasUpdated = playlist.update(req.body)
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('playlist', playlist)
|
await Database.updatePlaylist(playlist)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||||
}
|
}
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
@ -47,8 +48,8 @@ class PlaylistController {
|
|||||||
// DELETE: api/playlists/:id
|
// DELETE: api/playlists/:id
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
const playlist = req.playlist
|
const playlist = req.playlist
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.removeEntity('playlist', playlist.id)
|
await Database.removePlaylist(playlist.id)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -62,7 +63,7 @@ class PlaylistController {
|
|||||||
return res.status(400).send('Request body has no libraryItemId')
|
return res.status(400).send('Request body has no libraryItemId')
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
const libraryItem = Database.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(400).send('Library item not found')
|
return res.status(400).send('Library item not found')
|
||||||
}
|
}
|
||||||
@ -80,8 +81,16 @@ class PlaylistController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId)
|
playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId)
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
|
||||||
await this.db.updateEntity('playlist', playlist)
|
const playlistMediaItem = {
|
||||||
|
playlistId: playlist.id,
|
||||||
|
mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
|
||||||
|
mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
|
||||||
|
order: playlist.items.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
|
await Database.createPlaylistMediaItem(playlistMediaItem)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
@ -99,15 +108,15 @@ class PlaylistController {
|
|||||||
|
|
||||||
playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId)
|
playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId)
|
||||||
|
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
// Playlist is removed when there are no items
|
// Playlist is removed when there are no items
|
||||||
if (!playlist.items.length) {
|
if (!playlist.items.length) {
|
||||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||||
await this.db.removeEntity('playlist', playlist.id)
|
await Database.removePlaylist(playlist.id)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||||
} else {
|
} else {
|
||||||
await this.db.updateEntity('playlist', playlist)
|
await Database.updatePlaylist(playlist)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,20 +131,34 @@ class PlaylistController {
|
|||||||
}
|
}
|
||||||
const itemsToAdd = req.body.items
|
const itemsToAdd = req.body.items
|
||||||
let hasUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
|
let order = playlist.items.length
|
||||||
|
const playlistMediaItems = []
|
||||||
for (const item of itemsToAdd) {
|
for (const item of itemsToAdd) {
|
||||||
if (!item.libraryItemId) {
|
if (!item.libraryItemId) {
|
||||||
return res.status(400).send('Item does not have libraryItemId')
|
return res.status(400).send('Item does not have libraryItemId')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const libraryItem = Database.getLibraryItem(item.libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.status(400).send('Item not found with id ' + item.libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
if (!playlist.containsItem(item)) {
|
if (!playlist.containsItem(item)) {
|
||||||
|
playlistMediaItems.push({
|
||||||
|
playlistId: playlist.id,
|
||||||
|
mediaItemId: item.episodeId || libraryItem.media.id, // podcastEpisodeId or bookId
|
||||||
|
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||||
|
order: order++
|
||||||
|
})
|
||||||
playlist.addItem(item.libraryItemId, item.episodeId)
|
playlist.addItem(item.libraryItemId, item.episodeId)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('playlist', playlist)
|
await Database.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||||
}
|
}
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
@ -153,21 +176,22 @@ class PlaylistController {
|
|||||||
if (!item.libraryItemId) {
|
if (!item.libraryItemId) {
|
||||||
return res.status(400).send('Item does not have libraryItemId')
|
return res.status(400).send('Item does not have libraryItemId')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlist.containsItem(item)) {
|
if (playlist.containsItem(item)) {
|
||||||
playlist.removeItem(item.libraryItemId, item.episodeId)
|
playlist.removeItem(item.libraryItemId, item.episodeId)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
// Playlist is removed when there are no items
|
// Playlist is removed when there are no items
|
||||||
if (!playlist.items.length) {
|
if (!playlist.items.length) {
|
||||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||||
await this.db.removeEntity('playlist', playlist.id)
|
await Database.removePlaylist(playlist.id)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||||
} else {
|
} else {
|
||||||
await this.db.updateEntity('playlist', playlist)
|
await Database.updatePlaylist(playlist)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,12 +200,12 @@ class PlaylistController {
|
|||||||
|
|
||||||
// POST: api/playlists/collection/:collectionId
|
// POST: api/playlists/collection/:collectionId
|
||||||
async createFromCollection(req, res) {
|
async createFromCollection(req, res) {
|
||||||
let collection = this.db.collections.find(c => c.id === req.params.collectionId)
|
let collection = Database.collections.find(c => c.id === req.params.collectionId)
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return res.status(404).send('Collection not found')
|
return res.status(404).send('Collection not found')
|
||||||
}
|
}
|
||||||
// Expand collection to get library items
|
// Expand collection to get library items
|
||||||
collection = collection.toJSONExpanded(this.db.libraryItems)
|
collection = collection.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
// Filter out library items not accessible to user
|
// Filter out library items not accessible to user
|
||||||
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||||
@ -201,15 +225,15 @@ class PlaylistController {
|
|||||||
}
|
}
|
||||||
newPlaylist.setData(newPlaylistData)
|
newPlaylist.setData(newPlaylistData)
|
||||||
|
|
||||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||||
await this.db.insertEntity('playlist', newPlaylist)
|
await Database.createPlaylist(newPlaylist)
|
||||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
const playlist = this.db.playlists.find(p => p.id === req.params.id)
|
const playlist = Database.playlists.find(p => p.id === req.params.id)
|
||||||
if (!playlist) {
|
if (!playlist) {
|
||||||
return res.status(404).send('Playlist not found')
|
return res.status(404).send('Playlist not found')
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
const payload = req.body
|
const payload = req.body
|
||||||
|
|
||||||
const library = this.db.libraries.find(lib => lib.id === payload.libraryId)
|
const library = Database.libraries.find(lib => lib.id === payload.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
@ -33,7 +34,7 @@ class PodcastController {
|
|||||||
const podcastPath = filePathToPOSIX(payload.path)
|
const podcastPath = filePathToPOSIX(payload.path)
|
||||||
|
|
||||||
// Check if a library item with this podcast folder exists already
|
// Check if a library item with this podcast folder exists already
|
||||||
const existingLibraryItem = this.db.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
const existingLibraryItem = Database.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||||
if (existingLibraryItem) {
|
if (existingLibraryItem) {
|
||||||
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
||||||
return res.status(400).send('Podcast already exists')
|
return res.status(400).send('Podcast already exists')
|
||||||
@ -80,7 +81,7 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.insertLibraryItem(libraryItem)
|
await Database.createLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
res.json(libraryItem.toJSONExpanded())
|
res.json(libraryItem.toJSONExpanded())
|
||||||
@ -199,7 +200,7 @@ class PodcastController {
|
|||||||
const overrideDetails = req.query.override === '1'
|
const overrideDetails = req.query.override === '1'
|
||||||
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||||
if (episodesUpdated) {
|
if (episodesUpdated) {
|
||||||
await this.db.updateLibraryItem(req.libraryItem)
|
await Database.updateLibraryItem(req.libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,9 +217,8 @@ class PodcastController {
|
|||||||
return res.status(404).send('Episode not found')
|
return res.status(404).send('Episode not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
if (libraryItem.media.updateEpisode(episodeId, req.body)) {
|
||||||
if (wasUpdated) {
|
await Database.updateLibraryItem(libraryItem)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,13 +267,13 @@ class PodcastController {
|
|||||||
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
res.json(libraryItem.toJSON())
|
res.json(libraryItem.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
if (!item.isPodcast) {
|
if (!item.isPodcast) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const Database = require('../Database')
|
||||||
|
|
||||||
class RSSFeedController {
|
class RSSFeedController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -8,7 +8,7 @@ class RSSFeedController {
|
|||||||
async openRSSFeedForItem(req, res) {
|
async openRSSFeedForItem(req, res) {
|
||||||
const options = req.body || {}
|
const options = req.body || {}
|
||||||
|
|
||||||
const item = this.db.libraryItems.find(li => li.id === req.params.itemId)
|
const item = Database.libraryItems.find(li => li.id === req.params.itemId)
|
||||||
if (!item) return res.sendStatus(404)
|
if (!item) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
@ -45,7 +45,7 @@ class RSSFeedController {
|
|||||||
async openRSSFeedForCollection(req, res) {
|
async openRSSFeedForCollection(req, res) {
|
||||||
const options = req.body || {}
|
const options = req.body || {}
|
||||||
|
|
||||||
const collection = this.db.collections.find(li => li.id === req.params.collectionId)
|
const collection = Database.collections.find(li => li.id === req.params.collectionId)
|
||||||
if (!collection) return res.sendStatus(404)
|
if (!collection) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check request body options exist
|
// Check request body options exist
|
||||||
@ -60,7 +60,7 @@ class RSSFeedController {
|
|||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||||
|
|
||||||
// Check collection has audio tracks
|
// Check collection has audio tracks
|
||||||
@ -79,7 +79,7 @@ class RSSFeedController {
|
|||||||
async openRSSFeedForSeries(req, res) {
|
async openRSSFeedForSeries(req, res) {
|
||||||
const options = req.body || {}
|
const options = req.body || {}
|
||||||
|
|
||||||
const series = this.db.series.find(se => se.id === req.params.seriesId)
|
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check request body options exist
|
// Check request body options exist
|
||||||
@ -96,7 +96,7 @@ class RSSFeedController {
|
|||||||
|
|
||||||
const seriesJson = series.toJSON()
|
const seriesJson = series.toJSON()
|
||||||
// Get books in series that have audio tracks
|
// Get books in series that have audio tracks
|
||||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||||
|
|
||||||
// Check series has audio tracks
|
// Check series has audio tracks
|
||||||
if (!seriesJson.books.length) {
|
if (!seriesJson.books.length) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
class SeriesController {
|
class SeriesController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -35,7 +36,7 @@ class SeriesController {
|
|||||||
var q = (req.query.q || '').toLowerCase()
|
var q = (req.query.q || '').toLowerCase()
|
||||||
if (!q) return res.json([])
|
if (!q) return res.json([])
|
||||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||||
var series = this.db.series.filter(se => se.name.toLowerCase().includes(q))
|
var series = Database.series.filter(se => se.name.toLowerCase().includes(q))
|
||||||
series = series.slice(0, limit)
|
series = series.slice(0, limit)
|
||||||
res.json({
|
res.json({
|
||||||
results: series
|
results: series
|
||||||
@ -45,17 +46,17 @@ class SeriesController {
|
|||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const hasUpdated = req.series.update(req.body)
|
const hasUpdated = req.series.update(req.body)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('series', req.series)
|
await Database.updateSeries(req.series)
|
||||||
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||||
}
|
}
|
||||||
res.json(req.series.toJSON())
|
res.json(req.series.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
const series = this.db.series.find(se => se.id === req.params.id)
|
const series = Database.series.find(se => se.id === req.params.id)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
const libraryItemsInSeries = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||||
if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) {
|
if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) {
|
||||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user)
|
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
const { toNumber } = require('../utils/index')
|
const { toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class SessionController {
|
class SessionController {
|
||||||
@ -49,7 +50,7 @@ class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
||||||
const user = this.db.users.find(u => u.id === se.userId) || null
|
const user = Database.users.find(u => u.id === se.userId) || null
|
||||||
return {
|
return {
|
||||||
...se.toJSON(),
|
...se.toJSON(),
|
||||||
user: user ? { id: user.id, username: user.username } : null
|
user: user ? { id: user.id, username: user.username } : null
|
||||||
@ -62,7 +63,7 @@ class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOpenSession(req, res) {
|
getOpenSession(req, res) {
|
||||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
var libraryItem = Database.getLibraryItem(req.session.libraryItemId)
|
||||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||||
res.json(sessionForClient)
|
res.json(sessionForClient)
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ class SessionController {
|
|||||||
await this.playbackSessionManager.removeSession(req.session.id)
|
await this.playbackSessionManager.removeSession(req.session.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.removeEntity('session', req.session.id)
|
await Database.removePlaybackSession(req.session.id)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +116,7 @@ class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
const playbackSession = await this.db.getPlaybackSession(req.params.id)
|
const playbackSession = await Database.getPlaybackSession(req.params.id)
|
||||||
if (!playbackSession) {
|
if (!playbackSession) {
|
||||||
Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`)
|
Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
class ToolsController {
|
class ToolsController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -65,7 +66,7 @@ class ToolsController {
|
|||||||
|
|
||||||
const libraryItems = []
|
const libraryItems = []
|
||||||
for (const libraryItemId of libraryItemIds) {
|
for (const libraryItemId of libraryItemIds) {
|
||||||
const libraryItem = this.db.getLibraryItem(libraryItemId)
|
const libraryItem = Database.getLibraryItem(libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@ -105,7 +106,7 @@ class ToolsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const User = require('../objects/user/User')
|
const User = require('../objects/user/User')
|
||||||
|
|
||||||
const { getId, toNumber } = require('../utils/index')
|
const { toNumber } = require('../utils/index')
|
||||||
|
|
||||||
class UserController {
|
class UserController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -15,11 +17,11 @@ class UserController {
|
|||||||
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
||||||
|
|
||||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||||
const users = this.db.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||||
|
|
||||||
if (includes.includes('latestSession')) {
|
if (includes.includes('latestSession')) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
const userSessions = await this.db.selectUserSessions(user.id)
|
const userSessions = await Database.getPlaybackSessions({ userId: user.id })
|
||||||
user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null
|
user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +37,7 @@ class UserController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = this.db.users.find(u => u.id === req.params.id)
|
const user = Database.users.find(u => u.id === req.params.id)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
@ -47,18 +49,19 @@ class UserController {
|
|||||||
var account = req.body
|
var account = req.body
|
||||||
|
|
||||||
var username = account.username
|
var username = account.username
|
||||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
|
|
||||||
account.id = getId('usr')
|
account.id = uuidv4()
|
||||||
account.pash = await this.auth.hashPass(account.password)
|
account.pash = await this.auth.hashPass(account.password)
|
||||||
delete account.password
|
delete account.password
|
||||||
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
|
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
|
||||||
account.createdAt = Date.now()
|
account.createdAt = Date.now()
|
||||||
var newUser = new User(account)
|
const newUser = new User(account)
|
||||||
var success = await this.db.insertEntity('user', newUser)
|
|
||||||
|
const success = await Database.createUser(newUser)
|
||||||
if (success) {
|
if (success) {
|
||||||
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
||||||
res.json({
|
res.json({
|
||||||
@ -81,7 +84,7 @@ class UserController {
|
|||||||
var shouldUpdateToken = false
|
var shouldUpdateToken = false
|
||||||
|
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
if (account.username !== undefined && account.username !== user.username) {
|
||||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
@ -94,13 +97,12 @@ class UserController {
|
|||||||
delete account.password
|
delete account.password
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdated = user.update(account)
|
if (user.update(account)) {
|
||||||
if (hasUpdated) {
|
|
||||||
if (shouldUpdateToken) {
|
if (shouldUpdateToken) {
|
||||||
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
|
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', user)
|
await Database.updateUser(user)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,13 +126,13 @@ class UserController {
|
|||||||
// Todo: check if user is logged in and cancel streams
|
// Todo: check if user is logged in and cancel streams
|
||||||
|
|
||||||
// Remove user playlists
|
// Remove user playlists
|
||||||
const userPlaylists = this.db.playlists.filter(p => p.userId === user.id)
|
const userPlaylists = Database.playlists.filter(p => p.userId === user.id)
|
||||||
for (const playlist of userPlaylists) {
|
for (const playlist of userPlaylists) {
|
||||||
await this.db.removeEntity('playlist', playlist.id)
|
await Database.removePlaylist(playlist.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userJson = user.toJSONForBrowser()
|
const userJson = user.toJSONForBrowser()
|
||||||
await this.db.removeEntity('user', user.id)
|
await Database.removeUser(user.id)
|
||||||
SocketAuthority.adminEmitter('user_removed', userJson)
|
SocketAuthority.adminEmitter('user_removed', userJson)
|
||||||
res.json({
|
res.json({
|
||||||
success: true
|
success: true
|
||||||
@ -165,37 +167,39 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/users/:id/purge-media-progress
|
// POST: api/users/:id/purge-media-progress
|
||||||
|
// TODO: Remove
|
||||||
async purgeMediaProgress(req, res) {
|
async purgeMediaProgress(req, res) {
|
||||||
const user = req.reqUser
|
return res.sendStatus(404)
|
||||||
|
// const user = req.reqUser
|
||||||
|
|
||||||
if (user.type === 'root' && !req.user.isRoot) {
|
// if (user.type === 'root' && !req.user.isRoot) {
|
||||||
Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
// Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
||||||
return res.sendStatus(403)
|
// return res.sendStatus(403)
|
||||||
}
|
// }
|
||||||
|
|
||||||
var progressPurged = 0
|
// var progressPurged = 0
|
||||||
user.mediaProgress = user.mediaProgress.filter(mp => {
|
// user.mediaProgress = user.mediaProgress.filter(mp => {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
// const libraryItem = Database.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||||
if (!libraryItem) {
|
// if (!libraryItem) {
|
||||||
progressPurged++
|
// progressPurged++
|
||||||
return false
|
// return false
|
||||||
} else if (mp.episodeId) {
|
// } else if (mp.episodeId) {
|
||||||
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
// const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
||||||
if (!episode) { // Episode not found
|
// if (!episode) { // Episode not found
|
||||||
progressPurged++
|
// progressPurged++
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return true
|
// return true
|
||||||
})
|
// })
|
||||||
|
|
||||||
if (progressPurged) {
|
// if (progressPurged) {
|
||||||
Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
// Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
||||||
await this.db.updateEntity('user', user)
|
// await this.db.updateEntity('user', user)
|
||||||
SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
|
// SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
|
||||||
}
|
// }
|
||||||
|
|
||||||
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
// res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/users/online (admin)
|
// POST: api/users/online (admin)
|
||||||
@ -218,7 +222,7 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
req.reqUser = this.db.users.find(u => u.id === req.params.id)
|
req.reqUser = Database.users.find(u => u.id === req.params.id)
|
||||||
if (!req.reqUser) {
|
if (!req.reqUser) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
16
server/controllers2/libraryItem.controller.js
Normal file
16
server/controllers2/libraryItem.controller.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const itemDb = require('../db/item.db')
|
||||||
|
|
||||||
|
const getLibraryItem = async (req, res) => {
|
||||||
|
let libraryItem = null
|
||||||
|
if (req.query.expanded == 1) {
|
||||||
|
libraryItem = await itemDb.getLibraryItemExpanded(req.params.id)
|
||||||
|
} else {
|
||||||
|
libraryItem = await itemDb.getLibraryItemMinified(req.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLibraryItem
|
||||||
|
}
|
80
server/db/libraryItem.db.js
Normal file
80
server/db/libraryItem.db.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* TODO: Unused for testing
|
||||||
|
*/
|
||||||
|
const { Sequelize } = require('sequelize')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
|
const getLibraryItemMinified = (libraryItemId) => {
|
||||||
|
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags'
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.author,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
attributes: [
|
||||||
|
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags',
|
||||||
|
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLibraryItemExpanded = (libraryItemId) => {
|
||||||
|
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.podcast,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.podcastEpisode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'libraryFolder',
|
||||||
|
'library'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLibraryItemMinified,
|
||||||
|
getLibraryItemExpanded
|
||||||
|
}
|
@ -10,8 +10,7 @@ const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
|||||||
const toneHelpers = require('../utils/toneHelpers')
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
|
|
||||||
class AbMergeManager {
|
class AbMergeManager {
|
||||||
constructor(db, taskManager) {
|
constructor(taskManager) {
|
||||||
this.db = db
|
|
||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
|
|
||||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||||
|
@ -10,8 +10,7 @@ const toneHelpers = require('../utils/toneHelpers')
|
|||||||
const Task = require('../objects/Task')
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class AudioMetadataMangaer {
|
class AudioMetadataMangaer {
|
||||||
constructor(db, taskManager) {
|
constructor(taskManager) {
|
||||||
this.db = db
|
|
||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
|
|
||||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||||
|
@ -10,15 +10,14 @@ const { downloadFile, filePathToPOSIX } = require('../utils/fileUtils')
|
|||||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||||
|
|
||||||
class CoverManager {
|
class CoverManager {
|
||||||
constructor(db, cacheManager) {
|
constructor(cacheManager) {
|
||||||
this.db = db
|
|
||||||
this.cacheManager = cacheManager
|
this.cacheManager = cacheManager
|
||||||
|
|
||||||
this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items')
|
this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items')
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoverDirectory(libraryItem) {
|
getCoverDirectory(libraryItem) {
|
||||||
if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) {
|
if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) {
|
||||||
return libraryItem.path
|
return libraryItem.path
|
||||||
} else {
|
} else {
|
||||||
return Path.posix.join(this.ItemMetadataPath, libraryItem.id)
|
return Path.posix.join(this.ItemMetadataPath, libraryItem.id)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const cron = require('../libs/nodeCron')
|
const cron = require('../libs/nodeCron')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
class CronManager {
|
class CronManager {
|
||||||
constructor(db, scanner, podcastManager) {
|
constructor(scanner, podcastManager) {
|
||||||
this.db = db
|
|
||||||
this.scanner = scanner
|
this.scanner = scanner
|
||||||
this.podcastManager = podcastManager
|
this.podcastManager = podcastManager
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class CronManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initLibraryScanCrons() {
|
initLibraryScanCrons() {
|
||||||
for (const library of this.db.libraries) {
|
for (const library of Database.libraries) {
|
||||||
if (library.settings.autoScanCronExpression) {
|
if (library.settings.autoScanCronExpression) {
|
||||||
this.startCronForLibrary(library)
|
this.startCronForLibrary(library)
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ class CronManager {
|
|||||||
|
|
||||||
initPodcastCrons() {
|
initPodcastCrons() {
|
||||||
const cronExpressionMap = {}
|
const cronExpressionMap = {}
|
||||||
this.db.libraryItems.forEach((li) => {
|
Database.libraryItems.forEach((li) => {
|
||||||
if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) {
|
if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) {
|
||||||
if (!li.media.autoDownloadSchedule) {
|
if (!li.media.autoDownloadSchedule) {
|
||||||
Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`)
|
Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`)
|
||||||
@ -119,7 +119,7 @@ class CronManager {
|
|||||||
// Get podcast library items to check
|
// Get podcast library items to check
|
||||||
const libraryItems = []
|
const libraryItems = []
|
||||||
for (const libraryItemId of libraryItemIds) {
|
for (const libraryItemId of libraryItemIds) {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
||||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
const nodemailer = require('nodemailer')
|
const nodemailer = require('nodemailer')
|
||||||
const Logger = require("../Logger")
|
const Logger = require("../Logger")
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
|
||||||
|
|
||||||
class EmailManager {
|
class EmailManager {
|
||||||
constructor(db) {
|
constructor() { }
|
||||||
this.db = db
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransporter() {
|
getTransporter() {
|
||||||
return nodemailer.createTransport(this.db.emailSettings.getTransportObject())
|
return nodemailer.createTransport(Database.emailSettings.getTransportObject())
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTest(res) {
|
async sendTest(res) {
|
||||||
@ -25,8 +22,8 @@ class EmailManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transporter.sendMail({
|
transporter.sendMail({
|
||||||
from: this.db.emailSettings.fromAddress,
|
from: Database.emailSettings.fromAddress,
|
||||||
to: this.db.emailSettings.testAddress || this.db.emailSettings.fromAddress,
|
to: Database.emailSettings.testAddress || Database.emailSettings.fromAddress,
|
||||||
subject: 'Test email from Audiobookshelf',
|
subject: 'Test email from Audiobookshelf',
|
||||||
text: 'Success!'
|
text: 'Success!'
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
@ -52,7 +49,7 @@ class EmailManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transporter.sendMail({
|
transporter.sendMail({
|
||||||
from: this.db.emailSettings.fromAddress,
|
from: Database.emailSettings.fromAddress,
|
||||||
to: device.email,
|
to: device.email,
|
||||||
subject: "Here is your Ebook!",
|
subject: "Here is your Ebook!",
|
||||||
html: '<div dir="auto"></div>',
|
html: '<div dir="auto"></div>',
|
||||||
|
@ -9,9 +9,7 @@ const Logger = require('../Logger')
|
|||||||
const TAG = '[LogManager]'
|
const TAG = '[LogManager]'
|
||||||
|
|
||||||
class LogManager {
|
class LogManager {
|
||||||
constructor(db) {
|
constructor() {
|
||||||
this.db = db
|
|
||||||
|
|
||||||
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
||||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||||
|
|
||||||
@ -20,12 +18,8 @@ class LogManager {
|
|||||||
this.dailyLogFiles = []
|
this.dailyLogFiles = []
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverSettings() {
|
|
||||||
return this.db.serverSettings || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
get loggerDailyLogsToKeep() {
|
get loggerDailyLogsToKeep() {
|
||||||
return this.serverSettings.loggerDailyLogsToKeep || 7
|
return global.ServerSettings.loggerDailyLogsToKeep || 7
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLogDirs() {
|
async ensureLogDirs() {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const Logger = require("../Logger")
|
const Logger = require("../Logger")
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
const { notificationData } = require('../utils/notifications')
|
const { notificationData } = require('../utils/notifications')
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
constructor(db) {
|
constructor() {
|
||||||
this.db = db
|
|
||||||
|
|
||||||
this.sendingNotification = false
|
this.sendingNotification = false
|
||||||
this.notificationQueue = []
|
this.notificationQueue = []
|
||||||
}
|
}
|
||||||
@ -16,10 +15,10 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPodcastEpisodeDownloaded(libraryItem, episode) {
|
onPodcastEpisodeDownloaded(libraryItem, episode) {
|
||||||
if (!this.db.notificationSettings.isUseable) return
|
if (!Database.notificationSettings.isUseable) return
|
||||||
|
|
||||||
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
|
||||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
const eventData = {
|
const eventData = {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryId: libraryItem.libraryId,
|
libraryId: libraryItem.libraryId,
|
||||||
@ -42,19 +41,19 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async triggerNotification(eventName, eventData, intentionallyFail = false) {
|
async triggerNotification(eventName, eventData, intentionallyFail = false) {
|
||||||
if (!this.db.notificationSettings.isUseable) return
|
if (!Database.notificationSettings.isUseable) return
|
||||||
|
|
||||||
// Will queue the notification if sendingNotification and queue is not full
|
// Will queue the notification if sendingNotification and queue is not full
|
||||||
if (!this.checkTriggerNotification(eventName, eventData)) return
|
if (!this.checkTriggerNotification(eventName, eventData)) return
|
||||||
|
|
||||||
const notifications = this.db.notificationSettings.getActiveNotificationsForEvent(eventName)
|
const notifications = Database.notificationSettings.getActiveNotificationsForEvent(eventName)
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`)
|
Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`)
|
||||||
const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
|
const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
|
||||||
|
|
||||||
notification.updateNotificationFired(success)
|
notification.updateNotificationFired(success)
|
||||||
if (!success) { // Failed notification
|
if (!success) { // Failed notification
|
||||||
if (notification.numConsecutiveFailedAttempts >= this.db.notificationSettings.maxFailedAttempts) {
|
if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) {
|
||||||
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
|
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
|
||||||
notification.enabled = false
|
notification.enabled = false
|
||||||
} else {
|
} else {
|
||||||
@ -63,8 +62,8 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
await Database.updateSetting(Database.notificationSettings)
|
||||||
SocketAuthority.emitter('notifications_updated', this.db.notificationSettings.toJSON())
|
SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON())
|
||||||
|
|
||||||
this.notificationFinished()
|
this.notificationFinished()
|
||||||
}
|
}
|
||||||
@ -72,7 +71,7 @@ class NotificationManager {
|
|||||||
// Return TRUE if notification should be triggered now
|
// Return TRUE if notification should be triggered now
|
||||||
checkTriggerNotification(eventName, eventData) {
|
checkTriggerNotification(eventName, eventData) {
|
||||||
if (this.sendingNotification) {
|
if (this.sendingNotification) {
|
||||||
if (this.notificationQueue.length >= this.db.notificationSettings.maxNotificationQueue) {
|
if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) {
|
||||||
Logger.warn(`[NotificationManager] Notification queue is full - ignoring event ${eventName}`)
|
Logger.warn(`[NotificationManager] Notification queue is full - ignoring event ${eventName}`)
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`)
|
Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`)
|
||||||
@ -92,7 +91,7 @@ class NotificationManager {
|
|||||||
const nextNotificationEvent = this.notificationQueue.shift()
|
const nextNotificationEvent = this.notificationQueue.shift()
|
||||||
this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
|
this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
|
||||||
}
|
}
|
||||||
}, this.db.notificationSettings.notificationDelay)
|
}, Database.notificationSettings.notificationDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTestNotification(notification) {
|
sendTestNotification(notification) {
|
||||||
@ -107,7 +106,7 @@ class NotificationManager {
|
|||||||
|
|
||||||
sendNotification(notification, eventData) {
|
sendNotification(notification, eventData) {
|
||||||
const payload = notification.getApprisePayload(eventData)
|
const payload = notification.getApprisePayload(eventData)
|
||||||
return axios.post(this.db.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
|
return axios.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
|
||||||
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
|
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
@ -2,6 +2,7 @@ const Path = require('path')
|
|||||||
const serverVersion = require('../../package.json').version
|
const serverVersion = require('../../package.json').version
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const date = require('../libs/dateAndTime')
|
const date = require('../libs/dateAndTime')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
@ -15,8 +16,7 @@ const DeviceInfo = require('../objects/DeviceInfo')
|
|||||||
const Stream = require('../objects/Stream')
|
const Stream = require('../objects/Stream')
|
||||||
|
|
||||||
class PlaybackSessionManager {
|
class PlaybackSessionManager {
|
||||||
constructor(db) {
|
constructor() {
|
||||||
this.db = db
|
|
||||||
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
||||||
|
|
||||||
this.sessions = []
|
this.sessions = []
|
||||||
@ -33,19 +33,31 @@ class PlaybackSessionManager {
|
|||||||
return session?.stream || null
|
return session?.stream || null
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeviceInfo(req) {
|
async getDeviceInfo(req) {
|
||||||
const ua = uaParserJs(req.headers['user-agent'])
|
const ua = uaParserJs(req.headers['user-agent'])
|
||||||
const ip = requestIp.getClientIp(req)
|
const ip = requestIp.getClientIp(req)
|
||||||
|
|
||||||
const clientDeviceInfo = req.body?.deviceInfo || null
|
const clientDeviceInfo = req.body?.deviceInfo || null
|
||||||
|
|
||||||
const deviceInfo = new DeviceInfo()
|
const deviceInfo = new DeviceInfo()
|
||||||
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion)
|
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user.id)
|
||||||
|
|
||||||
|
if (clientDeviceInfo?.deviceId) {
|
||||||
|
const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId)
|
||||||
|
if (existingDevice) {
|
||||||
|
if (existingDevice.update(deviceInfo)) {
|
||||||
|
await Database.updateDevice(existingDevice)
|
||||||
|
}
|
||||||
|
return existingDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return deviceInfo
|
return deviceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSessionRequest(req, res, episodeId) {
|
async startSessionRequest(req, res, episodeId) {
|
||||||
const deviceInfo = this.getDeviceInfo(req)
|
const deviceInfo = await this.getDeviceInfo(req)
|
||||||
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
|
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
|
||||||
const { user, libraryItem, body: options } = req
|
const { user, libraryItem, body: options } = req
|
||||||
const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options)
|
const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options)
|
||||||
@ -77,7 +89,7 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSession(user, sessionJson) {
|
async syncLocalSession(user, sessionJson) {
|
||||||
const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
const libraryItem = Database.getLibraryItem(sessionJson.libraryItemId)
|
||||||
const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
||||||
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
||||||
@ -88,12 +100,12 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = await this.db.getPlaybackSession(sessionJson.id)
|
let session = await Database.getPlaybackSession(sessionJson.id)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// New session from local
|
// New session from local
|
||||||
session = new PlaybackSession(sessionJson)
|
session = new PlaybackSession(sessionJson)
|
||||||
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
|
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
|
||||||
await this.db.insertEntity('session', session)
|
await Database.createPlaybackSession(session)
|
||||||
} else {
|
} else {
|
||||||
session.currentTime = sessionJson.currentTime
|
session.currentTime = sessionJson.currentTime
|
||||||
session.timeListening = sessionJson.timeListening
|
session.timeListening = sessionJson.timeListening
|
||||||
@ -102,7 +114,7 @@ class PlaybackSessionManager {
|
|||||||
session.dayOfWeek = date.format(new Date(), 'dddd')
|
session.dayOfWeek = date.format(new Date(), 'dddd')
|
||||||
|
|
||||||
Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`)
|
Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`)
|
||||||
await this.db.updateEntity('session', session)
|
await Database.updatePlaybackSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
@ -126,8 +138,8 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
// Update user and emit socket event
|
// Update user and emit socket event
|
||||||
if (result.progressSynced) {
|
if (result.progressSynced) {
|
||||||
await this.db.updateEntity('user', user)
|
|
||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
|
if (itemProgress) await Database.upsertMediaProgress(itemProgress)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@ -155,7 +167,7 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
|
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
|
||||||
// Close any sessions already open for user and device
|
// Close any sessions already open for user and device
|
||||||
const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.deviceId)
|
const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.id)
|
||||||
for (const session of userSessions) {
|
for (const session of userSessions) {
|
||||||
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`)
|
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`)
|
||||||
await this.closeSession(user, session, null)
|
await this.closeSession(user, session, null)
|
||||||
@ -213,13 +225,13 @@ class PlaybackSessionManager {
|
|||||||
user.currentSessionId = newPlaybackSession.id
|
user.currentSessionId = newPlaybackSession.id
|
||||||
|
|
||||||
this.sessions.push(newPlaybackSession)
|
this.sessions.push(newPlaybackSession)
|
||||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems))
|
||||||
|
|
||||||
return newPlaybackSession
|
return newPlaybackSession
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncSession(user, session, syncData) {
|
async syncSession(user, session, syncData) {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
const libraryItem = Database.libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||||
return null
|
return null
|
||||||
@ -236,9 +248,8 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
|
|
||||||
await this.db.updateEntity('user', user)
|
|
||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
|
if (itemProgress) await Database.upsertMediaProgress(itemProgress)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
@ -259,7 +270,7 @@ class PlaybackSessionManager {
|
|||||||
await this.saveSession(session)
|
await this.saveSession(session)
|
||||||
}
|
}
|
||||||
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
|
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
|
||||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems))
|
||||||
SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id)
|
SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id)
|
||||||
return this.removeSession(session.id)
|
return this.removeSession(session.id)
|
||||||
}
|
}
|
||||||
@ -268,10 +279,10 @@ class PlaybackSessionManager {
|
|||||||
if (!session.timeListening) return // Do not save a session with no listening time
|
if (!session.timeListening) return // Do not save a session with no listening time
|
||||||
|
|
||||||
if (session.lastSave) {
|
if (session.lastSave) {
|
||||||
return this.db.updateEntity('session', session)
|
return Database.updatePlaybackSession(session)
|
||||||
} else {
|
} else {
|
||||||
session.lastSave = Date.now()
|
session.lastSave = Date.now()
|
||||||
return this.db.insertEntity('session', session)
|
return Database.createPlaybackSession(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,16 +316,5 @@ class PlaybackSessionManager {
|
|||||||
Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error)
|
Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android app v0.9.54 and below had a bug where listening time was sending unix timestamp
|
|
||||||
// See https://github.com/advplyr/audiobookshelf/issues/868
|
|
||||||
// Remove playback sessions with listening time too high
|
|
||||||
async removeInvalidSessions() {
|
|
||||||
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000
|
|
||||||
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
|
||||||
if (numSessionsRemoved) {
|
|
||||||
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = PlaybackSessionManager
|
module.exports = PlaybackSessionManager
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
@ -19,8 +20,7 @@ const AudioFile = require('../objects/files/AudioFile')
|
|||||||
const Task = require("../objects/Task")
|
const Task = require("../objects/Task")
|
||||||
|
|
||||||
class PodcastManager {
|
class PodcastManager {
|
||||||
constructor(db, watcher, notificationManager, taskManager) {
|
constructor(watcher, notificationManager, taskManager) {
|
||||||
this.db = db
|
|
||||||
this.watcher = watcher
|
this.watcher = watcher
|
||||||
this.notificationManager = notificationManager
|
this.notificationManager = notificationManager
|
||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
@ -32,10 +32,6 @@ class PodcastManager {
|
|||||||
this.MaxFailedEpisodeChecks = 24
|
this.MaxFailedEpisodeChecks = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverSettings() {
|
|
||||||
return this.db.serverSettings || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEpisodeDownloadsInQueue(libraryItemId) {
|
getEpisodeDownloadsInQueue(libraryItemId) {
|
||||||
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
@ -59,6 +55,7 @@ class PodcastManager {
|
|||||||
const newPe = new PodcastEpisode()
|
const newPe = new PodcastEpisode()
|
||||||
newPe.setData(ep, index++)
|
newPe.setData(ep, index++)
|
||||||
newPe.libraryItemId = libraryItem.id
|
newPe.libraryItemId = libraryItem.id
|
||||||
|
newPe.podcastId = libraryItem.media.id
|
||||||
const newPeDl = new PodcastEpisodeDownload()
|
const newPeDl = new PodcastEpisodeDownload()
|
||||||
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
||||||
this.startPodcastEpisodeDownload(newPeDl)
|
this.startPodcastEpisodeDownload(newPeDl)
|
||||||
@ -153,7 +150,7 @@ class PodcastManager {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
const libraryItem = Database.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
||||||
return false
|
return false
|
||||||
@ -182,7 +179,7 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded()
|
const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded()
|
||||||
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
|
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
|
||||||
@ -235,6 +232,7 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
const newAudioFile = new AudioFile()
|
const newAudioFile = new AudioFile()
|
||||||
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
||||||
|
newAudioFile.index = 1
|
||||||
return newAudioFile
|
return newAudioFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +272,7 @@ class PodcastManager {
|
|||||||
|
|
||||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
return libraryItem.media.autoDownloadEpisodes
|
return libraryItem.media.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
@ -313,7 +311,7 @@ class PodcastManager {
|
|||||||
|
|
||||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
return newEpisodes
|
return newEpisodes
|
||||||
|
@ -2,14 +2,13 @@ const Path = require('path')
|
|||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Feed = require('../objects/Feed')
|
const Feed = require('../objects/Feed')
|
||||||
|
|
||||||
class RssFeedManager {
|
class RssFeedManager {
|
||||||
constructor(db) {
|
constructor() {
|
||||||
this.db = db
|
|
||||||
|
|
||||||
this.feeds = {}
|
this.feeds = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,18 +18,18 @@ class RssFeedManager {
|
|||||||
|
|
||||||
validateFeedEntity(feedObj) {
|
validateFeedEntity(feedObj) {
|
||||||
if (feedObj.entityType === 'collection') {
|
if (feedObj.entityType === 'collection') {
|
||||||
if (!this.db.collections.some(li => li.id === feedObj.entityId)) {
|
if (!Database.collections.some(li => li.id === feedObj.entityId)) {
|
||||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (feedObj.entityType === 'libraryItem') {
|
} else if (feedObj.entityType === 'libraryItem') {
|
||||||
if (!this.db.libraryItems.some(li => li.id === feedObj.entityId)) {
|
if (!Database.libraryItems.some(li => li.id === feedObj.entityId)) {
|
||||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (feedObj.entityType === 'series') {
|
} else if (feedObj.entityType === 'series') {
|
||||||
const series = this.db.series.find(s => s.id === feedObj.entityId)
|
const series = Database.series.find(s => s.id === feedObj.entityId)
|
||||||
const hasSeriesBook = this.db.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
const hasSeriesBook = Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||||
if (!hasSeriesBook) {
|
if (!hasSeriesBook) {
|
||||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
|
||||||
return false
|
return false
|
||||||
@ -43,19 +42,13 @@ class RssFeedManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const feedObjects = await this.db.getAllEntities('feed')
|
const feedObjects = Database.feeds
|
||||||
if (!feedObjects || !feedObjects.length) return
|
if (!feedObjects?.length) return
|
||||||
|
|
||||||
for (const feedObj of feedObjects) {
|
for (const feedObj of feedObjects) {
|
||||||
// Migration: In v2.2.12 entityType "item" was updated to "libraryItem"
|
|
||||||
if (feedObj.entityType === 'item') {
|
|
||||||
feedObj.entityType = 'libraryItem'
|
|
||||||
await this.db.updateEntity('feed', feedObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove invalid feeds
|
// Remove invalid feeds
|
||||||
if (!this.validateFeedEntity(feedObj)) {
|
if (!this.validateFeedEntity(feedObj)) {
|
||||||
await this.db.removeEntity('feed', feedObj.id)
|
await Database.removeFeed(feedObj.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const feed = new Feed(feedObj)
|
const feed = new Feed(feedObj)
|
||||||
@ -82,7 +75,7 @@ class RssFeedManager {
|
|||||||
|
|
||||||
// Check if feed needs to be updated
|
// Check if feed needs to be updated
|
||||||
if (feed.entityType === 'libraryItem') {
|
if (feed.entityType === 'libraryItem') {
|
||||||
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
const libraryItem = Database.getLibraryItem(feed.entityId)
|
||||||
|
|
||||||
let mostRecentlyUpdatedAt = libraryItem.updatedAt
|
let mostRecentlyUpdatedAt = libraryItem.updatedAt
|
||||||
if (libraryItem.isPodcast) {
|
if (libraryItem.isPodcast) {
|
||||||
@ -94,12 +87,12 @@ class RssFeedManager {
|
|||||||
if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) {
|
if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) {
|
||||||
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
||||||
feed.updateFromItem(libraryItem)
|
feed.updateFromItem(libraryItem)
|
||||||
await this.db.updateEntity('feed', feed)
|
await Database.updateFeed(feed)
|
||||||
}
|
}
|
||||||
} else if (feed.entityType === 'collection') {
|
} else if (feed.entityType === 'collection') {
|
||||||
const collection = this.db.collections.find(c => c.id === feed.entityId)
|
const collection = Database.collections.find(c => c.id === feed.entityId)
|
||||||
if (collection) {
|
if (collection) {
|
||||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
// Find most recently updated item in collection
|
// Find most recently updated item in collection
|
||||||
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
||||||
@ -113,15 +106,15 @@ class RssFeedManager {
|
|||||||
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
|
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
|
||||||
|
|
||||||
feed.updateFromCollection(collectionExpanded)
|
feed.updateFromCollection(collectionExpanded)
|
||||||
await this.db.updateEntity('feed', feed)
|
await Database.updateFeed(feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (feed.entityType === 'series') {
|
} else if (feed.entityType === 'series') {
|
||||||
const series = this.db.series.find(s => s.id === feed.entityId)
|
const series = Database.series.find(s => s.id === feed.entityId)
|
||||||
if (series) {
|
if (series) {
|
||||||
const seriesJson = series.toJSON()
|
const seriesJson = series.toJSON()
|
||||||
// Get books in series that have audio tracks
|
// Get books in series that have audio tracks
|
||||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||||
|
|
||||||
// Find most recently updated item in series
|
// Find most recently updated item in series
|
||||||
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
||||||
@ -140,7 +133,7 @@ class RssFeedManager {
|
|||||||
Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`)
|
Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`)
|
||||||
|
|
||||||
feed.updateFromSeries(seriesJson)
|
feed.updateFromSeries(seriesJson)
|
||||||
await this.db.updateEntity('feed', feed)
|
await Database.updateFeed(feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +190,7 @@ class RssFeedManager {
|
|||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
await this.db.insertEntity('feed', feed)
|
await Database.createFeed(feed)
|
||||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
@ -214,7 +207,7 @@ class RssFeedManager {
|
|||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
await this.db.insertEntity('feed', feed)
|
await Database.createFeed(feed)
|
||||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
@ -231,14 +224,14 @@ class RssFeedManager {
|
|||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
await this.db.insertEntity('feed', feed)
|
await Database.createFeed(feed)
|
||||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCloseFeed(feed) {
|
async handleCloseFeed(feed) {
|
||||||
if (!feed) return
|
if (!feed) return
|
||||||
await this.db.removeEntity('feed', feed.id)
|
await Database.removeFeed(feed.id)
|
||||||
SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified())
|
SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified())
|
||||||
delete this.feeds[feed.id]
|
delete this.feeds[feed.id]
|
||||||
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
||||||
|
78
server/models/Author.js
Normal file
78
server/models/Author.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldAuthor = require('../objects/entities/Author')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Author extends Model {
|
||||||
|
static async getOldAuthors() {
|
||||||
|
const authors = await this.findAll()
|
||||||
|
return authors.map(au => au.getOldAuthor())
|
||||||
|
}
|
||||||
|
|
||||||
|
getOldAuthor() {
|
||||||
|
return new oldAuthor({
|
||||||
|
id: this.id,
|
||||||
|
asin: this.asin,
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
imagePath: this.imagePath,
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldAuthor) {
|
||||||
|
const author = this.getFromOld(oldAuthor)
|
||||||
|
return this.update(author, {
|
||||||
|
where: {
|
||||||
|
id: author.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldAuthor) {
|
||||||
|
const author = this.getFromOld(oldAuthor)
|
||||||
|
return this.create(author)
|
||||||
|
}
|
||||||
|
|
||||||
|
static createBulkFromOld(oldAuthors) {
|
||||||
|
const authors = oldAuthors.map(this.getFromOld)
|
||||||
|
return this.bulkCreate(authors)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldAuthor) {
|
||||||
|
return {
|
||||||
|
id: oldAuthor.id,
|
||||||
|
name: oldAuthor.name,
|
||||||
|
asin: oldAuthor.asin,
|
||||||
|
description: oldAuthor.description,
|
||||||
|
imagePath: oldAuthor.imagePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(authorId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: authorId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Author.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
asin: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
imagePath: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'author'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Author
|
||||||
|
}
|
121
server/models/Book.js
Normal file
121
server/models/Book.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Book extends Model {
|
||||||
|
static getOldBook(libraryItemExpanded) {
|
||||||
|
const bookExpanded = libraryItemExpanded.media
|
||||||
|
const authors = bookExpanded.authors.map(au => {
|
||||||
|
return {
|
||||||
|
id: au.id,
|
||||||
|
name: au.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const series = bookExpanded.series.map(se => {
|
||||||
|
return {
|
||||||
|
id: se.id,
|
||||||
|
name: se.name,
|
||||||
|
sequence: se.bookSeries.sequence
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
id: bookExpanded.id,
|
||||||
|
libraryItemId: libraryItemExpanded.id,
|
||||||
|
coverPath: bookExpanded.coverPath,
|
||||||
|
tags: bookExpanded.tags,
|
||||||
|
audioFiles: bookExpanded.audioFiles,
|
||||||
|
chapters: bookExpanded.chapters,
|
||||||
|
ebookFile: bookExpanded.ebookFile,
|
||||||
|
metadata: {
|
||||||
|
title: bookExpanded.title,
|
||||||
|
subtitle: bookExpanded.subtitle,
|
||||||
|
authors: authors,
|
||||||
|
narrators: bookExpanded.narrators,
|
||||||
|
series: series,
|
||||||
|
genres: bookExpanded.genres,
|
||||||
|
publishedYear: bookExpanded.publishedYear,
|
||||||
|
publishedDate: bookExpanded.publishedDate,
|
||||||
|
publisher: bookExpanded.publisher,
|
||||||
|
description: bookExpanded.description,
|
||||||
|
isbn: bookExpanded.isbn,
|
||||||
|
asin: bookExpanded.asin,
|
||||||
|
language: bookExpanded.language,
|
||||||
|
explicit: bookExpanded.explicit,
|
||||||
|
abridged: bookExpanded.abridged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} oldBook
|
||||||
|
* @returns {boolean} true if updated
|
||||||
|
*/
|
||||||
|
static saveFromOld(oldBook) {
|
||||||
|
const book = this.getFromOld(oldBook)
|
||||||
|
return this.update(book, {
|
||||||
|
where: {
|
||||||
|
id: book.id
|
||||||
|
}
|
||||||
|
}).then(result => result[0] > 0).catch((error) => {
|
||||||
|
Logger.error(`[Book] Failed to save book ${book.id}`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldBook) {
|
||||||
|
return {
|
||||||
|
id: oldBook.id,
|
||||||
|
title: oldBook.metadata.title,
|
||||||
|
subtitle: oldBook.metadata.subtitle,
|
||||||
|
publishedYear: oldBook.metadata.publishedYear,
|
||||||
|
publishedDate: oldBook.metadata.publishedDate,
|
||||||
|
publisher: oldBook.metadata.publisher,
|
||||||
|
description: oldBook.metadata.description,
|
||||||
|
isbn: oldBook.metadata.isbn,
|
||||||
|
asin: oldBook.metadata.asin,
|
||||||
|
language: oldBook.metadata.language,
|
||||||
|
explicit: !!oldBook.metadata.explicit,
|
||||||
|
abridged: !!oldBook.metadata.abridged,
|
||||||
|
narrators: oldBook.metadata.narrators,
|
||||||
|
ebookFile: oldBook.ebookFile?.toJSON() || null,
|
||||||
|
coverPath: oldBook.coverPath,
|
||||||
|
audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [],
|
||||||
|
chapters: oldBook.chapters,
|
||||||
|
tags: oldBook.tags,
|
||||||
|
genres: oldBook.metadata.genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Book.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
subtitle: DataTypes.STRING,
|
||||||
|
publishedYear: DataTypes.STRING,
|
||||||
|
publishedDate: DataTypes.STRING,
|
||||||
|
publisher: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
isbn: DataTypes.STRING,
|
||||||
|
asin: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
abridged: DataTypes.BOOLEAN,
|
||||||
|
coverPath: DataTypes.STRING,
|
||||||
|
|
||||||
|
narrators: DataTypes.JSON,
|
||||||
|
audioFiles: DataTypes.JSON,
|
||||||
|
ebookFile: DataTypes.JSON,
|
||||||
|
chapters: DataTypes.JSON,
|
||||||
|
tags: DataTypes.JSON,
|
||||||
|
genres: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'book'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Book
|
||||||
|
}
|
40
server/models/BookAuthor.js
Normal file
40
server/models/BookAuthor.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookAuthor extends Model {
|
||||||
|
static removeByIds(authorId = null, bookId = null) {
|
||||||
|
const where = {}
|
||||||
|
if (authorId) where.authorId = authorId
|
||||||
|
if (bookId) where.bookId = bookId
|
||||||
|
return this.destroy({
|
||||||
|
where
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookAuthor.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookAuthor',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, author } = sequelize.models
|
||||||
|
book.belongsToMany(author, { through: BookAuthor })
|
||||||
|
author.belongsToMany(book, { through: BookAuthor })
|
||||||
|
|
||||||
|
book.hasMany(BookAuthor)
|
||||||
|
BookAuthor.belongsTo(book)
|
||||||
|
|
||||||
|
author.hasMany(BookAuthor)
|
||||||
|
BookAuthor.belongsTo(author)
|
||||||
|
|
||||||
|
return BookAuthor
|
||||||
|
}
|
41
server/models/BookSeries.js
Normal file
41
server/models/BookSeries.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class BookSeries extends Model {
|
||||||
|
static removeByIds(seriesId = null, bookId = null) {
|
||||||
|
const where = {}
|
||||||
|
if (seriesId) where.seriesId = seriesId
|
||||||
|
if (bookId) where.bookId = bookId
|
||||||
|
return this.destroy({
|
||||||
|
where
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookSeries.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
sequence: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'bookSeries',
|
||||||
|
timestamps: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, series } = sequelize.models
|
||||||
|
book.belongsToMany(series, { through: BookSeries })
|
||||||
|
series.belongsToMany(book, { through: BookSeries })
|
||||||
|
|
||||||
|
book.hasMany(BookSeries)
|
||||||
|
BookSeries.belongsTo(book)
|
||||||
|
|
||||||
|
series.hasMany(BookSeries)
|
||||||
|
BookSeries.belongsTo(series)
|
||||||
|
|
||||||
|
return BookSeries
|
||||||
|
}
|
117
server/models/Collection.js
Normal file
117
server/models/Collection.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldCollection = require('../objects/Collection')
|
||||||
|
const { areEquivalent } = require('../utils/index')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Collection extends Model {
|
||||||
|
static async getOldCollections() {
|
||||||
|
const collections = await this.findAll({
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.book,
|
||||||
|
include: sequelize.models.libraryItem
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return collections.map(c => this.getOldCollection(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldCollection(collectionExpanded) {
|
||||||
|
const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || []
|
||||||
|
return new oldCollection({
|
||||||
|
id: collectionExpanded.id,
|
||||||
|
libraryId: collectionExpanded.libraryId,
|
||||||
|
name: collectionExpanded.name,
|
||||||
|
description: collectionExpanded.description,
|
||||||
|
books: libraryItemIds,
|
||||||
|
lastUpdate: collectionExpanded.updatedAt.valueOf(),
|
||||||
|
createdAt: collectionExpanded.createdAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldCollection) {
|
||||||
|
const collection = this.getFromOld(oldCollection)
|
||||||
|
return this.create(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fullUpdateFromOld(oldCollection, collectionBooks) {
|
||||||
|
const existingCollection = await this.findByPk(oldCollection.id, {
|
||||||
|
include: sequelize.models.collectionBook
|
||||||
|
})
|
||||||
|
if (!existingCollection) return false
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
const collection = this.getFromOld(oldCollection)
|
||||||
|
|
||||||
|
for (const cb of collectionBooks) {
|
||||||
|
const existingCb = existingCollection.collectionBooks.find(i => i.bookId === cb.bookId)
|
||||||
|
if (!existingCb) {
|
||||||
|
await sequelize.models.collectionBook.create(cb)
|
||||||
|
hasUpdates = true
|
||||||
|
} else if (existingCb.order != cb.order) {
|
||||||
|
await existingCb.update({ order: cb.order })
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const cb of existingCollection.collectionBooks) {
|
||||||
|
// collectionBook was removed
|
||||||
|
if (!collectionBooks.some(i => i.bookId === cb.bookId)) {
|
||||||
|
await cb.destroy()
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasCollectionUpdates = false
|
||||||
|
for (const key in collection) {
|
||||||
|
let existingValue = existingCollection[key]
|
||||||
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||||
|
if (!areEquivalent(collection[key], existingValue)) {
|
||||||
|
hasCollectionUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasCollectionUpdates) {
|
||||||
|
existingCollection.update(collection)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldCollection) {
|
||||||
|
return {
|
||||||
|
id: oldCollection.id,
|
||||||
|
name: oldCollection.name,
|
||||||
|
description: oldCollection.description,
|
||||||
|
createdAt: oldCollection.createdAt,
|
||||||
|
updatedAt: oldCollection.lastUpdate,
|
||||||
|
libraryId: oldCollection.libraryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(collectionId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: collectionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'collection'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library } = sequelize.models
|
||||||
|
|
||||||
|
library.hasMany(Collection)
|
||||||
|
Collection.belongsTo(library)
|
||||||
|
|
||||||
|
return Collection
|
||||||
|
}
|
42
server/models/CollectionBook.js
Normal file
42
server/models/CollectionBook.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class CollectionBook extends Model {
|
||||||
|
static removeByIds(collectionId, bookId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
bookId,
|
||||||
|
collectionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionBook.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
order: DataTypes.INTEGER
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
timestamps: true,
|
||||||
|
updatedAt: false,
|
||||||
|
modelName: 'collectionBook'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Super Many-to-Many
|
||||||
|
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||||
|
const { book, collection } = sequelize.models
|
||||||
|
book.belongsToMany(collection, { through: CollectionBook })
|
||||||
|
collection.belongsToMany(book, { through: CollectionBook })
|
||||||
|
|
||||||
|
book.hasMany(CollectionBook)
|
||||||
|
CollectionBook.belongsTo(book)
|
||||||
|
|
||||||
|
collection.hasMany(CollectionBook)
|
||||||
|
CollectionBook.belongsTo(collection)
|
||||||
|
|
||||||
|
return CollectionBook
|
||||||
|
}
|
112
server/models/Device.js
Normal file
112
server/models/Device.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const oldDevice = require('../objects/DeviceInfo')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Device extends Model {
|
||||||
|
getOldDevice() {
|
||||||
|
let browserVersion = null
|
||||||
|
let sdkVersion = null
|
||||||
|
if (this.clientName === 'Abs Android') {
|
||||||
|
sdkVersion = this.deviceVersion || null
|
||||||
|
} else {
|
||||||
|
browserVersion = this.deviceVersion || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return new oldDevice({
|
||||||
|
id: this.id,
|
||||||
|
deviceId: this.deviceId,
|
||||||
|
userId: this.userId,
|
||||||
|
ipAddress: this.ipAddress,
|
||||||
|
browserName: this.extraData.browserName || null,
|
||||||
|
browserVersion,
|
||||||
|
osName: this.extraData.osName || null,
|
||||||
|
osVersion: this.extraData.osVersion || null,
|
||||||
|
clientVersion: this.clientVersion || null,
|
||||||
|
manufacturer: this.extraData.manufacturer || null,
|
||||||
|
model: this.extraData.model || null,
|
||||||
|
sdkVersion,
|
||||||
|
deviceName: this.deviceName,
|
||||||
|
clientName: this.clientName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getOldDeviceByDeviceId(deviceId) {
|
||||||
|
const device = await this.findOne({
|
||||||
|
deviceId
|
||||||
|
})
|
||||||
|
if (!device) return null
|
||||||
|
return device.getOldDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldDevice) {
|
||||||
|
const device = this.getFromOld(oldDevice)
|
||||||
|
return this.create(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldDevice) {
|
||||||
|
const device = this.getFromOld(oldDevice)
|
||||||
|
return this.update(device, {
|
||||||
|
where: {
|
||||||
|
id: device.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldDeviceInfo) {
|
||||||
|
let extraData = {}
|
||||||
|
|
||||||
|
if (oldDeviceInfo.manufacturer) {
|
||||||
|
extraData.manufacturer = oldDeviceInfo.manufacturer
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.model) {
|
||||||
|
extraData.model = oldDeviceInfo.model
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.osName) {
|
||||||
|
extraData.osName = oldDeviceInfo.osName
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.osVersion) {
|
||||||
|
extraData.osVersion = oldDeviceInfo.osVersion
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.browserName) {
|
||||||
|
extraData.browserName = oldDeviceInfo.browserName
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: oldDeviceInfo.id,
|
||||||
|
deviceId: oldDeviceInfo.deviceId,
|
||||||
|
clientName: oldDeviceInfo.clientName || null,
|
||||||
|
clientVersion: oldDeviceInfo.clientVersion || null,
|
||||||
|
ipAddress: oldDeviceInfo.ipAddress,
|
||||||
|
deviceName: oldDeviceInfo.deviceName || null,
|
||||||
|
deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null,
|
||||||
|
userId: oldDeviceInfo.userId,
|
||||||
|
extraData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Device.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
deviceId: DataTypes.STRING,
|
||||||
|
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
|
||||||
|
clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
|
||||||
|
ipAddress: DataTypes.STRING,
|
||||||
|
deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||||
|
deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'device'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(Device)
|
||||||
|
Device.belongsTo(user)
|
||||||
|
|
||||||
|
return Device
|
||||||
|
}
|
165
server/models/Feed.js
Normal file
165
server/models/Feed.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const oldFeed = require('../objects/Feed')
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Feeds can be created from LibraryItem, Collection, Playlist or Series
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Feed extends Model {
|
||||||
|
static async getOldFeeds() {
|
||||||
|
const feeds = await this.findAll({
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.feedEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return feeds.map(f => this.getOldFeed(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldFeed(feedExpanded) {
|
||||||
|
const episodes = feedExpanded.feedEpisodes.map((feedEpisode) => feedEpisode.getOldEpisode())
|
||||||
|
|
||||||
|
return new oldFeed({
|
||||||
|
id: feedExpanded.id,
|
||||||
|
slug: feedExpanded.slug,
|
||||||
|
userId: feedExpanded.userId,
|
||||||
|
entityType: feedExpanded.entityType,
|
||||||
|
entityId: feedExpanded.entityId,
|
||||||
|
meta: {
|
||||||
|
title: feedExpanded.title,
|
||||||
|
description: feedExpanded.description,
|
||||||
|
author: feedExpanded.author,
|
||||||
|
imageUrl: feedExpanded.imageURL,
|
||||||
|
feedUrl: feedExpanded.feedURL,
|
||||||
|
link: feedExpanded.siteURL,
|
||||||
|
explicit: feedExpanded.explicit,
|
||||||
|
type: feedExpanded.podcastType,
|
||||||
|
language: feedExpanded.language,
|
||||||
|
preventIndexing: feedExpanded.preventIndexing,
|
||||||
|
ownerName: feedExpanded.ownerName,
|
||||||
|
ownerEmail: feedExpanded.ownerEmail
|
||||||
|
},
|
||||||
|
serverAddress: feedExpanded.serverAddress,
|
||||||
|
feedUrl: feedExpanded.feedURL,
|
||||||
|
episodes,
|
||||||
|
createdAt: feedExpanded.createdAt.valueOf(),
|
||||||
|
updatedAt: feedExpanded.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(feedId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: feedId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntity(options) {
|
||||||
|
if (!this.entityType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Feed.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
slug: DataTypes.STRING,
|
||||||
|
entityType: DataTypes.STRING,
|
||||||
|
entityId: DataTypes.UUIDV4,
|
||||||
|
entityUpdatedAt: DataTypes.DATE,
|
||||||
|
serverAddress: DataTypes.STRING,
|
||||||
|
feedURL: DataTypes.STRING,
|
||||||
|
imageURL: DataTypes.STRING,
|
||||||
|
siteURL: DataTypes.STRING,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
podcastType: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
ownerName: DataTypes.STRING,
|
||||||
|
ownerEmail: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
preventIndexing: DataTypes.BOOLEAN
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'feed'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(Feed)
|
||||||
|
Feed.belongsTo(user)
|
||||||
|
|
||||||
|
libraryItem.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'libraryItem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
collection.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'collection'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
series.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'series'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
playlist.hasMany(Feed, {
|
||||||
|
foreignKey: 'entityId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
entityType: 'playlist'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
||||||
|
|
||||||
|
Feed.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
|
||||||
|
instance.entity = instance.libraryItem
|
||||||
|
instance.dataValues.entity = instance.dataValues.libraryItem
|
||||||
|
} else if (instance.entityType === 'collection' && instance.collection !== undefined) {
|
||||||
|
instance.entity = instance.collection
|
||||||
|
instance.dataValues.entity = instance.dataValues.collection
|
||||||
|
} else if (instance.entityType === 'series' && instance.series !== undefined) {
|
||||||
|
instance.entity = instance.series
|
||||||
|
instance.dataValues.entity = instance.dataValues.series
|
||||||
|
} else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
|
||||||
|
instance.entity = instance.playlist
|
||||||
|
instance.dataValues.entity = instance.dataValues.playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.libraryItem
|
||||||
|
delete instance.dataValues.libraryItem
|
||||||
|
delete instance.collection
|
||||||
|
delete instance.dataValues.collection
|
||||||
|
delete instance.series
|
||||||
|
delete instance.dataValues.series
|
||||||
|
delete instance.playlist
|
||||||
|
delete instance.dataValues.playlist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Feed
|
||||||
|
}
|
60
server/models/FeedEpisode.js
Normal file
60
server/models/FeedEpisode.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class FeedEpisode extends Model {
|
||||||
|
getOldEpisode() {
|
||||||
|
const enclosure = {
|
||||||
|
url: this.enclosureURL,
|
||||||
|
size: this.enclosureSize,
|
||||||
|
type: this.enclosureType
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
enclosure,
|
||||||
|
pubDate: this.pubDate,
|
||||||
|
link: this.siteURL,
|
||||||
|
author: this.author,
|
||||||
|
explicit: this.explicit,
|
||||||
|
duration: this.duration,
|
||||||
|
season: this.season,
|
||||||
|
episode: this.episode,
|
||||||
|
episodeType: this.episodeType,
|
||||||
|
fullPath: this.filePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedEpisode.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
siteURL: DataTypes.STRING,
|
||||||
|
enclosureURL: DataTypes.STRING,
|
||||||
|
enclosureType: DataTypes.STRING,
|
||||||
|
enclosureSize: DataTypes.BIGINT,
|
||||||
|
pubDate: DataTypes.STRING,
|
||||||
|
season: DataTypes.STRING,
|
||||||
|
episode: DataTypes.STRING,
|
||||||
|
episodeType: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
filePath: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'feedEpisode'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { feed } = sequelize.models
|
||||||
|
|
||||||
|
feed.hasMany(FeedEpisode)
|
||||||
|
FeedEpisode.belongsTo(feed)
|
||||||
|
|
||||||
|
return FeedEpisode
|
||||||
|
}
|
137
server/models/Library.js
Normal file
137
server/models/Library.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldLibrary = require('../objects/Library')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Library extends Model {
|
||||||
|
static async getAllOldLibraries() {
|
||||||
|
const libraries = await this.findAll({
|
||||||
|
include: sequelize.models.libraryFolder
|
||||||
|
})
|
||||||
|
return libraries.map(lib => this.getOldLibrary(lib))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldLibrary(libraryExpanded) {
|
||||||
|
const folders = libraryExpanded.libraryFolders.map(folder => {
|
||||||
|
return {
|
||||||
|
id: folder.id,
|
||||||
|
fullPath: folder.path,
|
||||||
|
libraryId: folder.libraryId,
|
||||||
|
addedAt: folder.createdAt.valueOf()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return new oldLibrary({
|
||||||
|
id: libraryExpanded.id,
|
||||||
|
name: libraryExpanded.name,
|
||||||
|
folders,
|
||||||
|
displayOrder: libraryExpanded.displayOrder,
|
||||||
|
icon: libraryExpanded.icon,
|
||||||
|
mediaType: libraryExpanded.mediaType,
|
||||||
|
provider: libraryExpanded.provider,
|
||||||
|
settings: libraryExpanded.settings,
|
||||||
|
createdAt: libraryExpanded.createdAt.valueOf(),
|
||||||
|
lastUpdate: libraryExpanded.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} oldLibrary
|
||||||
|
* @returns {Library|null}
|
||||||
|
*/
|
||||||
|
static createFromOld(oldLibrary) {
|
||||||
|
const library = this.getFromOld(oldLibrary)
|
||||||
|
|
||||||
|
library.libraryFolders = oldLibrary.folders.map(folder => {
|
||||||
|
return {
|
||||||
|
id: folder.id,
|
||||||
|
path: folder.fullPath,
|
||||||
|
libraryId: library.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.create(library).catch((error) => {
|
||||||
|
Logger.error(`[Library] Failed to create library ${library.id}`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateFromOld(oldLibrary) {
|
||||||
|
const existingLibrary = await this.findByPk(oldLibrary.id, {
|
||||||
|
include: sequelize.models.libraryFolder
|
||||||
|
})
|
||||||
|
if (!existingLibrary) {
|
||||||
|
Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const library = this.getFromOld(oldLibrary)
|
||||||
|
|
||||||
|
const libraryFolders = oldLibrary.folders.map(folder => {
|
||||||
|
return {
|
||||||
|
id: folder.id,
|
||||||
|
path: folder.fullPath,
|
||||||
|
libraryId: library.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for (const libraryFolder of libraryFolders) {
|
||||||
|
const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id)
|
||||||
|
if (!existingLibraryFolder) {
|
||||||
|
await sequelize.models.libraryFolder.create(libraryFolder)
|
||||||
|
} else if (existingLibraryFolder.path !== libraryFolder.path) {
|
||||||
|
await existingLibraryFolder.update({ path: libraryFolder.path })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id))
|
||||||
|
for (const existingLibraryFolder of libraryFoldersRemoved) {
|
||||||
|
await existingLibraryFolder.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingLibrary.update(library)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldLibrary) {
|
||||||
|
return {
|
||||||
|
id: oldLibrary.id,
|
||||||
|
name: oldLibrary.name,
|
||||||
|
displayOrder: oldLibrary.displayOrder,
|
||||||
|
icon: oldLibrary.icon || null,
|
||||||
|
mediaType: oldLibrary.mediaType || null,
|
||||||
|
provider: oldLibrary.provider,
|
||||||
|
settings: oldLibrary.settings?.toJSON() || {},
|
||||||
|
createdAt: oldLibrary.createdAt,
|
||||||
|
updatedAt: oldLibrary.lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(libraryId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: libraryId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Library.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
displayOrder: DataTypes.INTEGER,
|
||||||
|
icon: DataTypes.STRING,
|
||||||
|
mediaType: DataTypes.STRING,
|
||||||
|
provider: DataTypes.STRING,
|
||||||
|
lastScan: DataTypes.DATE,
|
||||||
|
lastScanVersion: DataTypes.STRING,
|
||||||
|
settings: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'library'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Library
|
||||||
|
}
|
23
server/models/LibraryFolder.js
Normal file
23
server/models/LibraryFolder.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibraryFolder extends Model { }
|
||||||
|
|
||||||
|
LibraryFolder.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
path: DataTypes.STRING
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'libraryFolder'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library } = sequelize.models
|
||||||
|
library.hasMany(LibraryFolder)
|
||||||
|
LibraryFolder.belongsTo(library)
|
||||||
|
|
||||||
|
return LibraryFolder
|
||||||
|
}
|
393
server/models/LibraryItem.js
Normal file
393
server/models/LibraryItem.js
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const oldLibraryItem = require('../objects/LibraryItem')
|
||||||
|
const { areEquivalent } = require('../utils/index')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class LibraryItem extends Model {
|
||||||
|
static async getAllOldLibraryItems() {
|
||||||
|
let libraryItems = await this.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.book,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: sequelize.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: sequelize.models.podcast,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.podcastEpisode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
return libraryItems.map(ti => this.getOldLibraryItem(ti))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldLibraryItem(libraryItemExpanded) {
|
||||||
|
let media = null
|
||||||
|
if (libraryItemExpanded.mediaType === 'book') {
|
||||||
|
media = sequelize.models.book.getOldBook(libraryItemExpanded)
|
||||||
|
} else if (libraryItemExpanded.mediaType === 'podcast') {
|
||||||
|
media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new oldLibraryItem({
|
||||||
|
id: libraryItemExpanded.id,
|
||||||
|
ino: libraryItemExpanded.ino,
|
||||||
|
libraryId: libraryItemExpanded.libraryId,
|
||||||
|
folderId: libraryItemExpanded.libraryFolderId,
|
||||||
|
path: libraryItemExpanded.path,
|
||||||
|
relPath: libraryItemExpanded.relPath,
|
||||||
|
isFile: libraryItemExpanded.isFile,
|
||||||
|
mtimeMs: libraryItemExpanded.mtime?.valueOf(),
|
||||||
|
ctimeMs: libraryItemExpanded.ctime?.valueOf(),
|
||||||
|
birthtimeMs: libraryItemExpanded.birthtime?.valueOf(),
|
||||||
|
addedAt: libraryItemExpanded.createdAt.valueOf(),
|
||||||
|
updatedAt: libraryItemExpanded.updatedAt.valueOf(),
|
||||||
|
lastScan: libraryItemExpanded.lastScan?.valueOf(),
|
||||||
|
scanVersion: libraryItemExpanded.lastScanVersion,
|
||||||
|
isMissing: !!libraryItemExpanded.isMissing,
|
||||||
|
isInvalid: !!libraryItemExpanded.isInvalid,
|
||||||
|
mediaType: libraryItemExpanded.mediaType,
|
||||||
|
media,
|
||||||
|
libraryFiles: libraryItemExpanded.libraryFiles
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fullCreateFromOld(oldLibraryItem) {
|
||||||
|
const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem))
|
||||||
|
|
||||||
|
if (oldLibraryItem.mediaType === 'book') {
|
||||||
|
const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media)
|
||||||
|
bookObj.libraryItemId = newLibraryItem.id
|
||||||
|
const newBook = await sequelize.models.book.create(bookObj)
|
||||||
|
|
||||||
|
const oldBookAuthors = oldLibraryItem.media.metadata.authors || []
|
||||||
|
const oldBookSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||||
|
|
||||||
|
for (const oldBookAuthor of oldBookAuthors) {
|
||||||
|
await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id })
|
||||||
|
}
|
||||||
|
for (const oldSeries of oldBookSeriesAll) {
|
||||||
|
await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence })
|
||||||
|
}
|
||||||
|
} else if (oldLibraryItem.mediaType === 'podcast') {
|
||||||
|
const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
|
||||||
|
podcastObj.libraryItemId = newLibraryItem.id
|
||||||
|
const newPodcast = await sequelize.models.podcast.create(podcastObj)
|
||||||
|
|
||||||
|
const oldEpisodes = oldLibraryItem.media.episodes || []
|
||||||
|
for (const oldEpisode of oldEpisodes) {
|
||||||
|
const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode)
|
||||||
|
episodeObj.libraryItemId = newLibraryItem.id
|
||||||
|
episodeObj.podcastId = newPodcast.id
|
||||||
|
await sequelize.models.podcastEpisode.create(episodeObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLibraryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fullUpdateFromOld(oldLibraryItem) {
|
||||||
|
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.book,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: sequelize.models.series,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: sequelize.models.podcast,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.podcastEpisode
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!libraryItemExpanded) return false
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
|
||||||
|
// Check update Book/Podcast
|
||||||
|
if (libraryItemExpanded.media) {
|
||||||
|
let updatedMedia = null
|
||||||
|
if (libraryItemExpanded.mediaType === 'podcast') {
|
||||||
|
updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
|
||||||
|
|
||||||
|
const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || []
|
||||||
|
const updatedPodcastEpisodes = oldLibraryItem.media.episodes || []
|
||||||
|
|
||||||
|
for (const existingPodcastEpisode of existingPodcastEpisodes) {
|
||||||
|
// Episode was removed
|
||||||
|
if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`)
|
||||||
|
await existingPodcastEpisode.destroy()
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const updatedPodcastEpisode of updatedPodcastEpisodes) {
|
||||||
|
const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
|
||||||
|
if (!existingEpisodeMatch) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
|
||||||
|
await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
|
||||||
|
hasUpdates = true
|
||||||
|
} else {
|
||||||
|
const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode)
|
||||||
|
let episodeHasUpdates = false
|
||||||
|
for (const key in updatedEpisodeCleaned) {
|
||||||
|
let existingValue = existingEpisodeMatch[key]
|
||||||
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||||
|
|
||||||
|
if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`)
|
||||||
|
episodeHasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (episodeHasUpdates) {
|
||||||
|
await existingEpisodeMatch.update(updatedEpisodeCleaned)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (libraryItemExpanded.mediaType === 'book') {
|
||||||
|
updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media)
|
||||||
|
|
||||||
|
const existingAuthors = libraryItemExpanded.media.authors || []
|
||||||
|
const existingSeriesAll = libraryItemExpanded.media.series || []
|
||||||
|
const updatedAuthors = oldLibraryItem.media.metadata.authors || []
|
||||||
|
const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||||
|
|
||||||
|
for (const existingAuthor of existingAuthors) {
|
||||||
|
// Author was removed from Book
|
||||||
|
if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
|
||||||
|
await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const updatedAuthor of updatedAuthors) {
|
||||||
|
// Author was added
|
||||||
|
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
|
||||||
|
await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const existingSeries of existingSeriesAll) {
|
||||||
|
// Series was removed
|
||||||
|
if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
|
||||||
|
await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const updatedSeries of updatedSeriesAll) {
|
||||||
|
// Series was added/updated
|
||||||
|
const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
|
||||||
|
if (!existingSeriesMatch) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
|
||||||
|
await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
|
||||||
|
hasUpdates = true
|
||||||
|
} else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
|
||||||
|
await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence })
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasMediaUpdates = false
|
||||||
|
for (const key in updatedMedia) {
|
||||||
|
let existingValue = libraryItemExpanded.media[key]
|
||||||
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||||
|
|
||||||
|
if (!areEquivalent(updatedMedia[key], existingValue, true)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`)
|
||||||
|
hasMediaUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasMediaUpdates && updatedMedia) {
|
||||||
|
await libraryItemExpanded.media.update(updatedMedia)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedLibraryItem = this.getFromOld(oldLibraryItem)
|
||||||
|
let hasLibraryItemUpdates = false
|
||||||
|
for (const key in updatedLibraryItem) {
|
||||||
|
let existingValue = libraryItemExpanded[key]
|
||||||
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||||
|
|
||||||
|
if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
|
||||||
|
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
|
||||||
|
hasLibraryItemUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasLibraryItemUpdates) {
|
||||||
|
await libraryItemExpanded.update(updatedLibraryItem)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldLibraryItem) {
|
||||||
|
const libraryItem = this.getFromOld(oldLibraryItem)
|
||||||
|
return this.update(libraryItem, {
|
||||||
|
where: {
|
||||||
|
id: libraryItem.id
|
||||||
|
}
|
||||||
|
}).then((result) => result[0] > 0).catch((error) => {
|
||||||
|
Logger.error(`[LibraryItem] Failed to update libraryItem ${libraryItem.id}`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldLibraryItem) {
|
||||||
|
return {
|
||||||
|
id: oldLibraryItem.id,
|
||||||
|
ino: oldLibraryItem.ino,
|
||||||
|
path: oldLibraryItem.path,
|
||||||
|
relPath: oldLibraryItem.relPath,
|
||||||
|
mediaId: oldLibraryItem.media.id,
|
||||||
|
mediaType: oldLibraryItem.mediaType,
|
||||||
|
isFile: !!oldLibraryItem.isFile,
|
||||||
|
isMissing: !!oldLibraryItem.isMissing,
|
||||||
|
isInvalid: !!oldLibraryItem.isInvalid,
|
||||||
|
mtime: oldLibraryItem.mtimeMs,
|
||||||
|
ctime: oldLibraryItem.ctimeMs,
|
||||||
|
birthtime: oldLibraryItem.birthtimeMs,
|
||||||
|
lastScan: oldLibraryItem.lastScan,
|
||||||
|
lastScanVersion: oldLibraryItem.scanVersion,
|
||||||
|
createdAt: oldLibraryItem.addedAt,
|
||||||
|
updatedAt: oldLibraryItem.updatedAt,
|
||||||
|
libraryId: oldLibraryItem.libraryId,
|
||||||
|
libraryFolderId: oldLibraryItem.folderId,
|
||||||
|
libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(libraryItemId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: libraryItemId
|
||||||
|
},
|
||||||
|
individualHooks: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMedia(options) {
|
||||||
|
if (!this.mediaType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryItem.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
ino: DataTypes.STRING,
|
||||||
|
path: DataTypes.STRING,
|
||||||
|
relPath: DataTypes.STRING,
|
||||||
|
mediaId: DataTypes.UUIDV4,
|
||||||
|
mediaType: DataTypes.STRING,
|
||||||
|
isFile: DataTypes.BOOLEAN,
|
||||||
|
isMissing: DataTypes.BOOLEAN,
|
||||||
|
isInvalid: DataTypes.BOOLEAN,
|
||||||
|
mtime: DataTypes.DATE(6),
|
||||||
|
ctime: DataTypes.DATE(6),
|
||||||
|
birthtime: DataTypes.DATE(6),
|
||||||
|
lastScan: DataTypes.DATE,
|
||||||
|
lastScanVersion: DataTypes.STRING,
|
||||||
|
libraryFiles: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'libraryItem'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library, libraryFolder, book, podcast } = sequelize.models
|
||||||
|
library.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(library)
|
||||||
|
|
||||||
|
libraryFolder.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(libraryFolder)
|
||||||
|
|
||||||
|
book.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
podcast.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'podcast'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
LibraryItem.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.media = instance.book
|
||||||
|
instance.dataValues.media = instance.dataValues.book
|
||||||
|
} else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) {
|
||||||
|
instance.media = instance.podcast
|
||||||
|
instance.dataValues.media = instance.dataValues.podcast
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcast
|
||||||
|
delete instance.dataValues.podcast
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
LibraryItem.addHook('afterDestroy', async instance => {
|
||||||
|
if (!instance) return
|
||||||
|
const media = await instance.getMedia()
|
||||||
|
if (media) {
|
||||||
|
media.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return LibraryItem
|
||||||
|
}
|
141
server/models/MediaProgress.js
Normal file
141
server/models/MediaProgress.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||||
|
* Book has many MediaProgress. PodcastEpisode has many MediaProgress.
|
||||||
|
*/
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class MediaProgress extends Model {
|
||||||
|
getOldMediaProgress() {
|
||||||
|
const isPodcastEpisode = this.mediaItemType === 'podcastEpisode'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
userId: this.userId,
|
||||||
|
libraryItemId: this.extraData?.libraryItemId || null,
|
||||||
|
episodeId: isPodcastEpisode ? this.mediaItemId : null,
|
||||||
|
mediaItemId: this.mediaItemId,
|
||||||
|
mediaItemType: this.mediaItemType,
|
||||||
|
duration: this.duration,
|
||||||
|
progress: this.extraData?.progress || null,
|
||||||
|
currentTime: this.currentTime,
|
||||||
|
isFinished: !!this.isFinished,
|
||||||
|
hideFromContinueListening: !!this.hideFromContinueListening,
|
||||||
|
ebookLocation: this.ebookLocation,
|
||||||
|
ebookProgress: this.ebookProgress,
|
||||||
|
lastUpdate: this.updatedAt.valueOf(),
|
||||||
|
startedAt: this.createdAt.valueOf(),
|
||||||
|
finishedAt: this.finishedAt?.valueOf() || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static upsertFromOld(oldMediaProgress) {
|
||||||
|
const mediaProgress = this.getFromOld(oldMediaProgress)
|
||||||
|
return this.upsert(mediaProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldMediaProgress) {
|
||||||
|
return {
|
||||||
|
id: oldMediaProgress.id,
|
||||||
|
userId: oldMediaProgress.userId,
|
||||||
|
mediaItemId: oldMediaProgress.mediaItemId,
|
||||||
|
mediaItemType: oldMediaProgress.mediaItemType,
|
||||||
|
duration: oldMediaProgress.duration,
|
||||||
|
currentTime: oldMediaProgress.currentTime,
|
||||||
|
ebookLocation: oldMediaProgress.ebookLocation || null,
|
||||||
|
ebookProgress: oldMediaProgress.ebookProgress || null,
|
||||||
|
isFinished: !!oldMediaProgress.isFinished,
|
||||||
|
hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening,
|
||||||
|
finishedAt: oldMediaProgress.finishedAt,
|
||||||
|
createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate,
|
||||||
|
updatedAt: oldMediaProgress.lastUpdate,
|
||||||
|
extraData: {
|
||||||
|
libraryItemId: oldMediaProgress.libraryItemId,
|
||||||
|
progress: oldMediaProgress.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(mediaProgressId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: mediaProgressId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MediaProgress.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
currentTime: DataTypes.FLOAT,
|
||||||
|
isFinished: DataTypes.BOOLEAN,
|
||||||
|
hideFromContinueListening: DataTypes.BOOLEAN,
|
||||||
|
ebookLocation: DataTypes.STRING,
|
||||||
|
ebookProgress: DataTypes.FLOAT,
|
||||||
|
finishedAt: DataTypes.DATE,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'mediaProgress'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, user } = sequelize.models
|
||||||
|
|
||||||
|
book.hasMany(MediaProgress, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasMany(MediaProgress, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
MediaProgress.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user.hasMany(MediaProgress)
|
||||||
|
MediaProgress.belongsTo(user)
|
||||||
|
|
||||||
|
return MediaProgress
|
||||||
|
}
|
198
server/models/PlaybackSession.js
Normal file
198
server/models/PlaybackSession.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldPlaybackSession = require('../objects/PlaybackSession')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PlaybackSession extends Model {
|
||||||
|
static async getOldPlaybackSessions(where = null) {
|
||||||
|
const playbackSessions = await this.findAll({
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.device
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
return playbackSessions.map(session => this.getOldPlaybackSession(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getById(sessionId) {
|
||||||
|
const playbackSession = await this.findByPk(sessionId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.device
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!playbackSession) return null
|
||||||
|
return this.getOldPlaybackSession(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldPlaybackSession(playbackSessionExpanded) {
|
||||||
|
const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode'
|
||||||
|
|
||||||
|
return new oldPlaybackSession({
|
||||||
|
id: playbackSessionExpanded.id,
|
||||||
|
userId: playbackSessionExpanded.userId,
|
||||||
|
libraryId: playbackSessionExpanded.libraryId,
|
||||||
|
libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null,
|
||||||
|
bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId,
|
||||||
|
episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null,
|
||||||
|
mediaType: isPodcastEpisode ? 'podcast' : 'book',
|
||||||
|
mediaMetadata: playbackSessionExpanded.mediaMetadata,
|
||||||
|
chapters: null,
|
||||||
|
displayTitle: playbackSessionExpanded.displayTitle,
|
||||||
|
displayAuthor: playbackSessionExpanded.displayAuthor,
|
||||||
|
coverPath: playbackSessionExpanded.coverPath,
|
||||||
|
duration: playbackSessionExpanded.duration,
|
||||||
|
playMethod: playbackSessionExpanded.playMethod,
|
||||||
|
mediaPlayer: playbackSessionExpanded.mediaPlayer,
|
||||||
|
deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null,
|
||||||
|
serverVersion: playbackSessionExpanded.serverVersion,
|
||||||
|
date: playbackSessionExpanded.date,
|
||||||
|
dayOfWeek: playbackSessionExpanded.dayOfWeek,
|
||||||
|
timeListening: playbackSessionExpanded.timeListening,
|
||||||
|
startTime: playbackSessionExpanded.startTime,
|
||||||
|
currentTime: playbackSessionExpanded.currentTime,
|
||||||
|
startedAt: playbackSessionExpanded.createdAt.valueOf(),
|
||||||
|
updatedAt: playbackSessionExpanded.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(sessionId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: sessionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldPlaybackSession) {
|
||||||
|
const playbackSession = this.getFromOld(oldPlaybackSession)
|
||||||
|
return this.create(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldPlaybackSession) {
|
||||||
|
const playbackSession = this.getFromOld(oldPlaybackSession)
|
||||||
|
return this.update(playbackSession, {
|
||||||
|
where: {
|
||||||
|
id: playbackSession.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldPlaybackSession) {
|
||||||
|
return {
|
||||||
|
id: oldPlaybackSession.id,
|
||||||
|
mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId,
|
||||||
|
mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book',
|
||||||
|
libraryId: oldPlaybackSession.libraryId,
|
||||||
|
displayTitle: oldPlaybackSession.displayTitle,
|
||||||
|
displayAuthor: oldPlaybackSession.displayAuthor,
|
||||||
|
duration: oldPlaybackSession.duration,
|
||||||
|
playMethod: oldPlaybackSession.playMethod,
|
||||||
|
mediaPlayer: oldPlaybackSession.mediaPlayer,
|
||||||
|
startTime: oldPlaybackSession.startTime,
|
||||||
|
currentTime: oldPlaybackSession.currentTime,
|
||||||
|
serverVersion: oldPlaybackSession.serverVersion || null,
|
||||||
|
createdAt: oldPlaybackSession.startedAt,
|
||||||
|
updatedAt: oldPlaybackSession.updatedAt,
|
||||||
|
userId: oldPlaybackSession.userId,
|
||||||
|
deviceId: oldPlaybackSession.deviceInfo?.id || null,
|
||||||
|
timeListening: oldPlaybackSession.timeListening,
|
||||||
|
coverPath: oldPlaybackSession.coverPath,
|
||||||
|
mediaMetadata: oldPlaybackSession.mediaMetadata,
|
||||||
|
date: oldPlaybackSession.date,
|
||||||
|
dayOfWeek: oldPlaybackSession.dayOfWeek,
|
||||||
|
extraData: {
|
||||||
|
libraryItemId: oldPlaybackSession.libraryItemId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackSession.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
displayTitle: DataTypes.STRING,
|
||||||
|
displayAuthor: DataTypes.STRING,
|
||||||
|
duration: DataTypes.FLOAT,
|
||||||
|
playMethod: DataTypes.INTEGER,
|
||||||
|
mediaPlayer: DataTypes.STRING,
|
||||||
|
startTime: DataTypes.FLOAT,
|
||||||
|
currentTime: DataTypes.FLOAT,
|
||||||
|
serverVersion: DataTypes.STRING,
|
||||||
|
coverPath: DataTypes.STRING,
|
||||||
|
timeListening: DataTypes.INTEGER,
|
||||||
|
mediaMetadata: DataTypes.JSON,
|
||||||
|
date: DataTypes.STRING,
|
||||||
|
dayOfWeek: DataTypes.STRING,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'playbackSession'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, user, device, library } = sequelize.models
|
||||||
|
|
||||||
|
user.hasMany(PlaybackSession)
|
||||||
|
PlaybackSession.belongsTo(user)
|
||||||
|
|
||||||
|
device.hasMany(PlaybackSession)
|
||||||
|
PlaybackSession.belongsTo(device)
|
||||||
|
|
||||||
|
library.hasMany(PlaybackSession)
|
||||||
|
PlaybackSession.belongsTo(library)
|
||||||
|
|
||||||
|
book.hasMany(PlaybackSession, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasOne(PlaybackSession, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
PlaybackSession.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return PlaybackSession
|
||||||
|
}
|
171
server/models/Playlist.js
Normal file
171
server/models/Playlist.js
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldPlaylist = require('../objects/Playlist')
|
||||||
|
const { areEquivalent } = require('../utils/index')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Playlist extends Model {
|
||||||
|
static async getOldPlaylists() {
|
||||||
|
const playlists = await this.findAll({
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.playlistMediaItem,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: sequelize.models.book,
|
||||||
|
include: sequelize.models.libraryItem
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: sequelize.models.podcastEpisode,
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.podcast,
|
||||||
|
include: sequelize.models.libraryItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return playlists.map(p => this.getOldPlaylist(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldPlaylist(playlistExpanded) {
|
||||||
|
const items = playlistExpanded.playlistMediaItems.map(pmi => {
|
||||||
|
const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null
|
||||||
|
if (!libraryItemId) {
|
||||||
|
console.log(JSON.stringify(pmi, null, 2))
|
||||||
|
throw new Error('No library item id')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '',
|
||||||
|
libraryItemId: libraryItemId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return new oldPlaylist({
|
||||||
|
id: playlistExpanded.id,
|
||||||
|
libraryId: playlistExpanded.libraryId,
|
||||||
|
userId: playlistExpanded.userId,
|
||||||
|
name: playlistExpanded.name,
|
||||||
|
description: playlistExpanded.description,
|
||||||
|
items,
|
||||||
|
lastUpdate: playlistExpanded.updatedAt.valueOf(),
|
||||||
|
createdAt: playlistExpanded.createdAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldPlaylist) {
|
||||||
|
const playlist = this.getFromOld(oldPlaylist)
|
||||||
|
return this.create(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fullUpdateFromOld(oldPlaylist, playlistMediaItems) {
|
||||||
|
const existingPlaylist = await this.findByPk(oldPlaylist.id, {
|
||||||
|
include: sequelize.models.playlistMediaItem
|
||||||
|
})
|
||||||
|
if (!existingPlaylist) return false
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
const playlist = this.getFromOld(oldPlaylist)
|
||||||
|
|
||||||
|
for (const pmi of playlistMediaItems) {
|
||||||
|
const existingPmi = existingPlaylist.playlistMediaItems.find(i => i.mediaItemId === pmi.mediaItemId)
|
||||||
|
if (!existingPmi) {
|
||||||
|
await sequelize.models.playlistMediaItem.create(pmi)
|
||||||
|
hasUpdates = true
|
||||||
|
} else if (existingPmi.order != pmi.order) {
|
||||||
|
await existingPmi.update({ order: pmi.order })
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const pmi of existingPlaylist.playlistMediaItems) {
|
||||||
|
// Pmi was removed
|
||||||
|
if (!playlistMediaItems.some(i => i.mediaItemId === pmi.mediaItemId)) {
|
||||||
|
await pmi.destroy()
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasPlaylistUpdates = false
|
||||||
|
for (const key in playlist) {
|
||||||
|
let existingValue = existingPlaylist[key]
|
||||||
|
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||||
|
|
||||||
|
if (!areEquivalent(playlist[key], existingValue)) {
|
||||||
|
hasPlaylistUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasPlaylistUpdates) {
|
||||||
|
existingPlaylist.update(playlist)
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldPlaylist) {
|
||||||
|
return {
|
||||||
|
id: oldPlaylist.id,
|
||||||
|
name: oldPlaylist.name,
|
||||||
|
description: oldPlaylist.description,
|
||||||
|
createdAt: oldPlaylist.createdAt,
|
||||||
|
updatedAt: oldPlaylist.lastUpdate,
|
||||||
|
userId: oldPlaylist.userId,
|
||||||
|
libraryId: oldPlaylist.libraryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(playlistId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: playlistId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'playlist'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { library, user } = sequelize.models
|
||||||
|
library.hasMany(Playlist)
|
||||||
|
Playlist.belongsTo(library)
|
||||||
|
|
||||||
|
user.hasMany(Playlist)
|
||||||
|
Playlist.belongsTo(user)
|
||||||
|
|
||||||
|
Playlist.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.playlistMediaItems?.length) {
|
||||||
|
instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => {
|
||||||
|
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
||||||
|
pmi.mediaItem = pmi.book
|
||||||
|
pmi.dataValues.mediaItem = pmi.dataValues.book
|
||||||
|
} else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) {
|
||||||
|
pmi.mediaItem = pmi.podcastEpisode
|
||||||
|
pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete pmi.book
|
||||||
|
delete pmi.dataValues.book
|
||||||
|
delete pmi.podcastEpisode
|
||||||
|
delete pmi.dataValues.podcastEpisode
|
||||||
|
return pmi
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Playlist
|
||||||
|
}
|
82
server/models/PlaylistMediaItem.js
Normal file
82
server/models/PlaylistMediaItem.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PlaylistMediaItem extends Model {
|
||||||
|
static removeByIds(playlistId, mediaItemId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
playlistId,
|
||||||
|
mediaItemId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMediaItem(options) {
|
||||||
|
if (!this.mediaItemType) return Promise.resolve(null)
|
||||||
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||||
|
return this[mixinMethodName](options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistMediaItem.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
mediaItemId: DataTypes.UUIDV4,
|
||||||
|
mediaItemType: DataTypes.STRING,
|
||||||
|
order: DataTypes.INTEGER
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
timestamps: true,
|
||||||
|
updatedAt: false,
|
||||||
|
modelName: 'playlistMediaItem'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { book, podcastEpisode, playlist } = sequelize.models
|
||||||
|
|
||||||
|
book.hasMany(PlaylistMediaItem, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
podcastEpisode.hasOne(PlaylistMediaItem, {
|
||||||
|
foreignKey: 'mediaItemId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaItemType: 'podcastEpisode'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||||
|
|
||||||
|
PlaylistMediaItem.addHook('afterFind', findResult => {
|
||||||
|
if (!findResult) return
|
||||||
|
|
||||||
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
|
for (const instance of findResult) {
|
||||||
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||||
|
instance.mediaItem = instance.book
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.book
|
||||||
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||||
|
instance.mediaItem = instance.podcastEpisode
|
||||||
|
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
// To prevent mistakes:
|
||||||
|
delete instance.book
|
||||||
|
delete instance.dataValues.book
|
||||||
|
delete instance.podcastEpisode
|
||||||
|
delete instance.dataValues.podcastEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
playlist.hasMany(PlaylistMediaItem)
|
||||||
|
PlaylistMediaItem.belongsTo(playlist)
|
||||||
|
|
||||||
|
return PlaylistMediaItem
|
||||||
|
}
|
98
server/models/Podcast.js
Normal file
98
server/models/Podcast.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Podcast extends Model {
|
||||||
|
static getOldPodcast(libraryItemExpanded) {
|
||||||
|
const podcastExpanded = libraryItemExpanded.media
|
||||||
|
const podcastEpisodes = podcastExpanded.podcastEpisodes.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index)
|
||||||
|
return {
|
||||||
|
id: podcastExpanded.id,
|
||||||
|
libraryItemId: libraryItemExpanded.id,
|
||||||
|
metadata: {
|
||||||
|
title: podcastExpanded.title,
|
||||||
|
author: podcastExpanded.author,
|
||||||
|
description: podcastExpanded.description,
|
||||||
|
releaseDate: podcastExpanded.releaseDate,
|
||||||
|
genres: podcastExpanded.genres,
|
||||||
|
feedUrl: podcastExpanded.feedURL,
|
||||||
|
imageUrl: podcastExpanded.imageURL,
|
||||||
|
itunesPageUrl: podcastExpanded.itunesPageURL,
|
||||||
|
itunesId: podcastExpanded.itunesId,
|
||||||
|
itunesArtistId: podcastExpanded.itunesArtistId,
|
||||||
|
explicit: podcastExpanded.explicit,
|
||||||
|
language: podcastExpanded.language,
|
||||||
|
type: podcastExpanded.podcastType
|
||||||
|
},
|
||||||
|
coverPath: podcastExpanded.coverPath,
|
||||||
|
tags: podcastExpanded.tags,
|
||||||
|
episodes: podcastEpisodes,
|
||||||
|
autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: podcastExpanded.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null,
|
||||||
|
maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldPodcast) {
|
||||||
|
const oldPodcastMetadata = oldPodcast.metadata
|
||||||
|
return {
|
||||||
|
id: oldPodcast.id,
|
||||||
|
title: oldPodcastMetadata.title,
|
||||||
|
author: oldPodcastMetadata.author,
|
||||||
|
releaseDate: oldPodcastMetadata.releaseDate,
|
||||||
|
feedURL: oldPodcastMetadata.feedUrl,
|
||||||
|
imageURL: oldPodcastMetadata.imageUrl,
|
||||||
|
description: oldPodcastMetadata.description,
|
||||||
|
itunesPageURL: oldPodcastMetadata.itunesPageUrl,
|
||||||
|
itunesId: oldPodcastMetadata.itunesId,
|
||||||
|
itunesArtistId: oldPodcastMetadata.itunesArtistId,
|
||||||
|
language: oldPodcastMetadata.language,
|
||||||
|
podcastType: oldPodcastMetadata.type,
|
||||||
|
explicit: !!oldPodcastMetadata.explicit,
|
||||||
|
autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
|
||||||
|
maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
|
||||||
|
coverPath: oldPodcast.coverPath,
|
||||||
|
tags: oldPodcast.tags,
|
||||||
|
genres: oldPodcastMetadata.genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Podcast.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
author: DataTypes.STRING,
|
||||||
|
releaseDate: DataTypes.STRING,
|
||||||
|
feedURL: DataTypes.STRING,
|
||||||
|
imageURL: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
itunesPageURL: DataTypes.STRING,
|
||||||
|
itunesId: DataTypes.STRING,
|
||||||
|
itunesArtistId: DataTypes.STRING,
|
||||||
|
language: DataTypes.STRING,
|
||||||
|
podcastType: DataTypes.STRING,
|
||||||
|
explicit: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
|
autoDownloadEpisodes: DataTypes.BOOLEAN,
|
||||||
|
autoDownloadSchedule: DataTypes.STRING,
|
||||||
|
lastEpisodeCheck: DataTypes.DATE,
|
||||||
|
maxEpisodesToKeep: DataTypes.INTEGER,
|
||||||
|
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||||
|
coverPath: DataTypes.STRING,
|
||||||
|
tags: DataTypes.JSON,
|
||||||
|
genres: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcast'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Podcast
|
||||||
|
}
|
95
server/models/PodcastEpisode.js
Normal file
95
server/models/PodcastEpisode.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class PodcastEpisode extends Model {
|
||||||
|
getOldPodcastEpisode(libraryItemId = null) {
|
||||||
|
let enclosure = null
|
||||||
|
if (this.enclosureURL) {
|
||||||
|
enclosure = {
|
||||||
|
url: this.enclosureURL,
|
||||||
|
type: this.enclosureType,
|
||||||
|
length: this.enclosureSize !== null ? String(this.enclosureSize) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryItemId: libraryItemId || null,
|
||||||
|
podcastId: this.podcastId,
|
||||||
|
id: this.id,
|
||||||
|
index: this.index,
|
||||||
|
season: this.season,
|
||||||
|
episode: this.episode,
|
||||||
|
episodeType: this.episodeType,
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
description: this.description,
|
||||||
|
enclosure,
|
||||||
|
pubDate: this.pubDate,
|
||||||
|
chapters: this.chapters,
|
||||||
|
audioFile: this.audioFile,
|
||||||
|
publishedAt: this.publishedAt?.valueOf() || null,
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldEpisode) {
|
||||||
|
const podcastEpisode = this.getFromOld(oldEpisode)
|
||||||
|
return this.create(podcastEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldEpisode) {
|
||||||
|
return {
|
||||||
|
id: oldEpisode.id,
|
||||||
|
index: oldEpisode.index,
|
||||||
|
season: oldEpisode.season,
|
||||||
|
episode: oldEpisode.episode,
|
||||||
|
episodeType: oldEpisode.episodeType,
|
||||||
|
title: oldEpisode.title,
|
||||||
|
subtitle: oldEpisode.subtitle,
|
||||||
|
description: oldEpisode.description,
|
||||||
|
pubDate: oldEpisode.pubDate,
|
||||||
|
enclosureURL: oldEpisode.enclosure?.url || null,
|
||||||
|
enclosureSize: oldEpisode.enclosure?.length || null,
|
||||||
|
enclosureType: oldEpisode.enclosure?.type || null,
|
||||||
|
publishedAt: oldEpisode.publishedAt,
|
||||||
|
createdAt: oldEpisode.addedAt,
|
||||||
|
updatedAt: oldEpisode.updatedAt,
|
||||||
|
podcastId: oldEpisode.podcastId,
|
||||||
|
audioFile: oldEpisode.audioFile?.toJSON() || null,
|
||||||
|
chapters: oldEpisode.chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PodcastEpisode.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
index: DataTypes.INTEGER,
|
||||||
|
season: DataTypes.STRING,
|
||||||
|
episode: DataTypes.STRING,
|
||||||
|
episodeType: DataTypes.STRING,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
subtitle: DataTypes.STRING(1000),
|
||||||
|
description: DataTypes.TEXT,
|
||||||
|
pubDate: DataTypes.STRING,
|
||||||
|
enclosureURL: DataTypes.STRING,
|
||||||
|
enclosureSize: DataTypes.BIGINT,
|
||||||
|
enclosureType: DataTypes.STRING,
|
||||||
|
publishedAt: DataTypes.DATE,
|
||||||
|
|
||||||
|
audioFile: DataTypes.JSON,
|
||||||
|
chapters: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'podcastEpisode'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { podcast } = sequelize.models
|
||||||
|
podcast.hasMany(PodcastEpisode)
|
||||||
|
PodcastEpisode.belongsTo(podcast)
|
||||||
|
|
||||||
|
return PodcastEpisode
|
||||||
|
}
|
72
server/models/Series.js
Normal file
72
server/models/Series.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldSeries = require('../objects/entities/Series')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Series extends Model {
|
||||||
|
static async getAllOldSeries() {
|
||||||
|
const series = await this.findAll()
|
||||||
|
return series.map(se => se.getOldSeries())
|
||||||
|
}
|
||||||
|
|
||||||
|
getOldSeries() {
|
||||||
|
return new oldSeries({
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
addedAt: this.createdAt.valueOf(),
|
||||||
|
updatedAt: this.updatedAt.valueOf()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldSeries) {
|
||||||
|
const series = this.getFromOld(oldSeries)
|
||||||
|
return this.update(series, {
|
||||||
|
where: {
|
||||||
|
id: series.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldSeries) {
|
||||||
|
const series = this.getFromOld(oldSeries)
|
||||||
|
return this.create(series)
|
||||||
|
}
|
||||||
|
|
||||||
|
static createBulkFromOld(oldSeriesObjs) {
|
||||||
|
const series = oldSeriesObjs.map(this.getFromOld)
|
||||||
|
return this.bulkCreate(series)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldSeries) {
|
||||||
|
return {
|
||||||
|
id: oldSeries.id,
|
||||||
|
name: oldSeries.name,
|
||||||
|
description: oldSeries.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(seriesId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: seriesId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Series.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
description: DataTypes.TEXT
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'series'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Series
|
||||||
|
}
|
45
server/models/Setting.js
Normal file
45
server/models/Setting.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
|
const oldEmailSettings = require('../objects/settings/EmailSettings')
|
||||||
|
const oldServerSettings = require('../objects/settings/ServerSettings')
|
||||||
|
const oldNotificationSettings = require('../objects/settings/NotificationSettings')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class Setting extends Model {
|
||||||
|
static async getOldSettings() {
|
||||||
|
const settings = (await this.findAll()).map(se => se.value)
|
||||||
|
|
||||||
|
|
||||||
|
const emailSettingsJson = settings.find(se => se.id === 'email-settings')
|
||||||
|
const serverSettingsJson = settings.find(se => se.id === 'server-settings')
|
||||||
|
const notificationSettingsJson = settings.find(se => se.id === 'notification-settings')
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
emailSettings: new oldEmailSettings(emailSettingsJson),
|
||||||
|
serverSettings: new oldServerSettings(serverSettingsJson),
|
||||||
|
notificationSettings: new oldNotificationSettings(notificationSettingsJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateSettingObj(setting) {
|
||||||
|
return this.upsert({
|
||||||
|
key: setting.id,
|
||||||
|
value: setting
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.init({
|
||||||
|
key: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
value: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'setting'
|
||||||
|
})
|
||||||
|
|
||||||
|
return Setting
|
||||||
|
}
|
136
server/models/User.js
Normal file
136
server/models/User.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const oldUser = require('../objects/user/User')
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
class User extends Model {
|
||||||
|
static async getOldUsers() {
|
||||||
|
const users = await this.findAll({
|
||||||
|
include: sequelize.models.mediaProgress
|
||||||
|
})
|
||||||
|
return users.map(u => this.getOldUser(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOldUser(userExpanded) {
|
||||||
|
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
||||||
|
|
||||||
|
const librariesAccessible = userExpanded.permissions?.librariesAccessible || []
|
||||||
|
const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || []
|
||||||
|
const permissions = userExpanded.permissions || {}
|
||||||
|
delete permissions.librariesAccessible
|
||||||
|
delete permissions.itemTagsSelected
|
||||||
|
|
||||||
|
return new oldUser({
|
||||||
|
id: userExpanded.id,
|
||||||
|
oldUserId: userExpanded.extraData?.oldUserId || null,
|
||||||
|
username: userExpanded.username,
|
||||||
|
pash: userExpanded.pash,
|
||||||
|
type: userExpanded.type,
|
||||||
|
token: userExpanded.token,
|
||||||
|
mediaProgress,
|
||||||
|
seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [],
|
||||||
|
bookmarks: userExpanded.bookmarks,
|
||||||
|
isActive: userExpanded.isActive,
|
||||||
|
isLocked: userExpanded.isLocked,
|
||||||
|
lastSeen: userExpanded.lastSeen?.valueOf() || null,
|
||||||
|
createdAt: userExpanded.createdAt.valueOf(),
|
||||||
|
permissions,
|
||||||
|
librariesAccessible,
|
||||||
|
itemTagsSelected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromOld(oldUser) {
|
||||||
|
const user = this.getFromOld(oldUser)
|
||||||
|
return this.create(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateFromOld(oldUser) {
|
||||||
|
const user = this.getFromOld(oldUser)
|
||||||
|
return this.update(user, {
|
||||||
|
where: {
|
||||||
|
id: user.id
|
||||||
|
}
|
||||||
|
}).then((result) => result[0] > 0).catch((error) => {
|
||||||
|
Logger.error(`[User] Failed to save user ${oldUser.id}`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFromOld(oldUser) {
|
||||||
|
return {
|
||||||
|
id: oldUser.id,
|
||||||
|
username: oldUser.username,
|
||||||
|
pash: oldUser.pash || null,
|
||||||
|
type: oldUser.type || null,
|
||||||
|
token: oldUser.token || null,
|
||||||
|
isActive: !!oldUser.isActive,
|
||||||
|
lastSeen: oldUser.lastSeen || null,
|
||||||
|
extraData: {
|
||||||
|
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||||
|
oldUserId: oldUser.oldUserId
|
||||||
|
},
|
||||||
|
createdAt: oldUser.createdAt || Date.now(),
|
||||||
|
permissions: {
|
||||||
|
...oldUser.permissions,
|
||||||
|
librariesAccessible: oldUser.librariesAccessible || [],
|
||||||
|
itemTagsSelected: oldUser.itemTagsSelected || []
|
||||||
|
},
|
||||||
|
bookmarks: oldUser.bookmarks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeById(userId) {
|
||||||
|
return this.destroy({
|
||||||
|
where: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createRootUser(username, pash, token) {
|
||||||
|
const newRoot = new oldUser({
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'root',
|
||||||
|
username,
|
||||||
|
pash,
|
||||||
|
token,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: Date.now()
|
||||||
|
})
|
||||||
|
await this.createFromOld(newRoot)
|
||||||
|
return newRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
User.init({
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
username: DataTypes.STRING,
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
pash: DataTypes.STRING,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
token: DataTypes.STRING,
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
isLocked: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
lastSeen: DataTypes.DATE,
|
||||||
|
permissions: DataTypes.JSON,
|
||||||
|
bookmarks: DataTypes.JSON,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
|
}, {
|
||||||
|
sequelize,
|
||||||
|
modelName: 'user'
|
||||||
|
})
|
||||||
|
|
||||||
|
return User
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
const { getId } = require('../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class Collection {
|
class Collection {
|
||||||
constructor(collection) {
|
constructor(collection) {
|
||||||
this.id = null
|
this.id = null
|
||||||
this.libraryId = null
|
this.libraryId = null
|
||||||
this.userId = null
|
|
||||||
|
|
||||||
this.name = null
|
this.name = null
|
||||||
this.description = null
|
this.description = null
|
||||||
@ -25,7 +24,6 @@ class Collection {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
userId: this.userId,
|
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
cover: this.cover,
|
cover: this.cover,
|
||||||
@ -60,7 +58,6 @@ class Collection {
|
|||||||
construct(collection) {
|
construct(collection) {
|
||||||
this.id = collection.id
|
this.id = collection.id
|
||||||
this.libraryId = collection.libraryId
|
this.libraryId = collection.libraryId
|
||||||
this.userId = collection.userId
|
|
||||||
this.name = collection.name
|
this.name = collection.name
|
||||||
this.description = collection.description || null
|
this.description = collection.description || null
|
||||||
this.cover = collection.cover || null
|
this.cover = collection.cover || null
|
||||||
@ -71,11 +68,10 @@ class Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
if (!data.userId || !data.libraryId || !data.name) {
|
if (!data.libraryId || !data.name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
this.id = getId('col')
|
this.id = uuidv4()
|
||||||
this.userId = data.userId
|
|
||||||
this.libraryId = data.libraryId
|
this.libraryId = data.libraryId
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.description = data.description || null
|
this.description = data.description || null
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class DeviceInfo {
|
class DeviceInfo {
|
||||||
constructor(deviceInfo = null) {
|
constructor(deviceInfo = null) {
|
||||||
|
this.id = null
|
||||||
|
this.userId = null
|
||||||
this.deviceId = null
|
this.deviceId = null
|
||||||
this.ipAddress = null
|
this.ipAddress = null
|
||||||
|
|
||||||
@ -16,7 +20,8 @@ class DeviceInfo {
|
|||||||
this.model = null
|
this.model = null
|
||||||
this.sdkVersion = null // Android Only
|
this.sdkVersion = null // Android Only
|
||||||
|
|
||||||
this.serverVersion = null
|
this.clientName = null
|
||||||
|
this.deviceName = null
|
||||||
|
|
||||||
if (deviceInfo) {
|
if (deviceInfo) {
|
||||||
this.construct(deviceInfo)
|
this.construct(deviceInfo)
|
||||||
@ -33,6 +38,8 @@ class DeviceInfo {
|
|||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
const obj = {
|
const obj = {
|
||||||
|
id: this.id,
|
||||||
|
userId: this.userId,
|
||||||
deviceId: this.deviceId,
|
deviceId: this.deviceId,
|
||||||
ipAddress: this.ipAddress,
|
ipAddress: this.ipAddress,
|
||||||
browserName: this.browserName,
|
browserName: this.browserName,
|
||||||
@ -44,7 +51,8 @@ class DeviceInfo {
|
|||||||
manufacturer: this.manufacturer,
|
manufacturer: this.manufacturer,
|
||||||
model: this.model,
|
model: this.model,
|
||||||
sdkVersion: this.sdkVersion,
|
sdkVersion: this.sdkVersion,
|
||||||
serverVersion: this.serverVersion
|
clientName: this.clientName,
|
||||||
|
deviceName: this.deviceName
|
||||||
}
|
}
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj[key] === null || obj[key] === undefined) {
|
if (obj[key] === null || obj[key] === undefined) {
|
||||||
@ -65,6 +73,7 @@ class DeviceInfo {
|
|||||||
// When client doesn't send a device id
|
// When client doesn't send a device id
|
||||||
getTempDeviceId() {
|
getTempDeviceId() {
|
||||||
const keys = [
|
const keys = [
|
||||||
|
this.userId,
|
||||||
this.browserName,
|
this.browserName,
|
||||||
this.browserVersion,
|
this.browserVersion,
|
||||||
this.osName,
|
this.osName,
|
||||||
@ -78,7 +87,9 @@ class DeviceInfo {
|
|||||||
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(ip, ua, clientDeviceInfo, serverVersion) {
|
setData(ip, ua, clientDeviceInfo, serverVersion, userId) {
|
||||||
|
this.id = uuidv4()
|
||||||
|
this.userId = userId
|
||||||
this.deviceId = clientDeviceInfo?.deviceId || null
|
this.deviceId = clientDeviceInfo?.deviceId || null
|
||||||
this.ipAddress = ip || null
|
this.ipAddress = ip || null
|
||||||
|
|
||||||
@ -88,16 +99,54 @@ class DeviceInfo {
|
|||||||
this.osVersion = ua?.os.version || null
|
this.osVersion = ua?.os.version || null
|
||||||
this.deviceType = ua?.device.type || null
|
this.deviceType = ua?.device.type || null
|
||||||
|
|
||||||
this.clientVersion = clientDeviceInfo?.clientVersion || null
|
this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion
|
||||||
this.manufacturer = clientDeviceInfo?.manufacturer || null
|
this.manufacturer = clientDeviceInfo?.manufacturer || null
|
||||||
this.model = clientDeviceInfo?.model || null
|
this.model = clientDeviceInfo?.model || null
|
||||||
this.sdkVersion = clientDeviceInfo?.sdkVersion || null
|
this.sdkVersion = clientDeviceInfo?.sdkVersion || null
|
||||||
|
|
||||||
this.serverVersion = serverVersion || null
|
this.clientName = clientDeviceInfo?.clientName || null
|
||||||
|
if (this.sdkVersion) {
|
||||||
|
if (!this.clientName) this.clientName = 'Abs Android'
|
||||||
|
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||||
|
} else if (this.model) {
|
||||||
|
if (!this.clientName) this.clientName = 'Abs iOS'
|
||||||
|
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||||
|
} else if (this.osName && this.browserName) {
|
||||||
|
if (!this.clientName) this.clientName = 'Abs Web'
|
||||||
|
this.deviceName = `${this.osName} ${this.osVersion || 'N/A'} ${this.browserName}`
|
||||||
|
} else if (!this.clientName) {
|
||||||
|
this.clientName = 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.deviceId) {
|
if (!this.deviceId) {
|
||||||
this.deviceId = this.getTempDeviceId()
|
this.deviceId = this.getTempDeviceId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(deviceInfo) {
|
||||||
|
const deviceInfoJson = deviceInfo.toJSON ? deviceInfo.toJSON() : deviceInfo
|
||||||
|
const existingDeviceInfoJson = this.toJSON()
|
||||||
|
|
||||||
|
let hasUpdates = false
|
||||||
|
for (const key in deviceInfoJson) {
|
||||||
|
if (['id', 'deviceId'].includes(key)) continue
|
||||||
|
|
||||||
|
if (deviceInfoJson[key] !== existingDeviceInfoJson[key]) {
|
||||||
|
this[key] = deviceInfoJson[key]
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in existingDeviceInfoJson) {
|
||||||
|
if (['id', 'deviceId'].includes(key)) continue
|
||||||
|
|
||||||
|
if (existingDeviceInfoJson[key] && !deviceInfoJson[key]) {
|
||||||
|
this[key] = null
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = DeviceInfo
|
module.exports = DeviceInfo
|
@ -1,3 +1,4 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const FeedMeta = require('./FeedMeta')
|
const FeedMeta = require('./FeedMeta')
|
||||||
const FeedEpisode = require('./FeedEpisode')
|
const FeedEpisode = require('./FeedEpisode')
|
||||||
const RSS = require('../libs/rss')
|
const RSS = require('../libs/rss')
|
||||||
@ -90,7 +91,7 @@ class Feed {
|
|||||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||||
|
|
||||||
this.id = slug
|
this.id = uuidv4()
|
||||||
this.slug = slug
|
this.slug = slug
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.entityType = 'libraryItem'
|
this.entityType = 'libraryItem'
|
||||||
@ -179,7 +180,7 @@ class Feed {
|
|||||||
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||||
|
|
||||||
this.id = slug
|
this.id = uuidv4()
|
||||||
this.slug = slug
|
this.slug = slug
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.entityType = 'collection'
|
this.entityType = 'collection'
|
||||||
@ -253,7 +254,7 @@ class Feed {
|
|||||||
const libraryId = itemsWithTracks[0].libraryId
|
const libraryId = itemsWithTracks[0].libraryId
|
||||||
const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
|
const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
|
||||||
|
|
||||||
this.id = slug
|
this.id = uuidv4()
|
||||||
this.slug = slug
|
this.slug = slug
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.entityType = 'series'
|
this.entityType = 'series'
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
const Path = require('path')
|
|
||||||
const date = require('../libs/dateAndTime')
|
const date = require('../libs/dateAndTime')
|
||||||
const { secondsToTimestamp } = require('../utils/index')
|
const { secondsToTimestamp } = require('../utils/index')
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { getId } = require("../utils")
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class Folder {
|
class Folder {
|
||||||
constructor(folder = null) {
|
constructor(folder = null) {
|
||||||
@ -29,7 +29,7 @@ class Folder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
this.id = data.id ? data.id : getId('fol')
|
this.id = data.id || uuidv4()
|
||||||
this.fullPath = data.fullPath
|
this.fullPath = data.fullPath
|
||||||
this.libraryId = data.libraryId
|
this.libraryId = data.libraryId
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const Folder = require('./Folder')
|
const Folder = require('./Folder')
|
||||||
const LibrarySettings = require('./settings/LibrarySettings')
|
const LibrarySettings = require('./settings/LibrarySettings')
|
||||||
const { getId } = require('../utils/index')
|
|
||||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class Library {
|
class Library {
|
||||||
@ -87,7 +87,7 @@ class Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
this.id = data.id ? data.id : getId('lib')
|
this.id = data.id || uuidv4()
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
if (data.folder) {
|
if (data.folder) {
|
||||||
this.folders = [
|
this.folders = [
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
@ -8,7 +9,7 @@ const Book = require('./mediaTypes/Book')
|
|||||||
const Podcast = require('./mediaTypes/Podcast')
|
const Podcast = require('./mediaTypes/Podcast')
|
||||||
const Video = require('./mediaTypes/Video')
|
const Video = require('./mediaTypes/Video')
|
||||||
const Music = require('./mediaTypes/Music')
|
const Music = require('./mediaTypes/Music')
|
||||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index')
|
||||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class LibraryItem {
|
class LibraryItem {
|
||||||
@ -191,7 +192,7 @@ class LibraryItem {
|
|||||||
|
|
||||||
// Data comes from scandir library item data
|
// Data comes from scandir library item data
|
||||||
setData(libraryMediaType, payload) {
|
setData(libraryMediaType, payload) {
|
||||||
this.id = getId('li')
|
this.id = uuidv4()
|
||||||
this.mediaType = libraryMediaType
|
this.mediaType = libraryMediaType
|
||||||
if (libraryMediaType === 'video') {
|
if (libraryMediaType === 'video') {
|
||||||
this.media = new Video()
|
this.media = new Video()
|
||||||
@ -202,6 +203,7 @@ class LibraryItem {
|
|||||||
} else if (libraryMediaType === 'music') {
|
} else if (libraryMediaType === 'music') {
|
||||||
this.media = new Music()
|
this.media = new Music()
|
||||||
}
|
}
|
||||||
|
this.media.id = uuidv4()
|
||||||
this.media.libraryItemId = this.id
|
this.media.libraryItemId = this.id
|
||||||
|
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { getId } = require('../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
constructor(notification = null) {
|
constructor(notification = null) {
|
||||||
@ -57,7 +57,7 @@ class Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(payload) {
|
setData(payload) {
|
||||||
this.id = getId('noti')
|
this.id = uuidv4()
|
||||||
this.libraryId = payload.libraryId || null
|
this.libraryId = payload.libraryId || null
|
||||||
this.eventName = payload.eventName
|
this.eventName = payload.eventName
|
||||||
this.urls = payload.urls
|
this.urls = payload.urls
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const date = require('../libs/dateAndTime')
|
const date = require('../libs/dateAndTime')
|
||||||
const { getId } = require('../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
|
const serverVersion = require('../../package.json').version
|
||||||
const BookMetadata = require('./metadata/BookMetadata')
|
const BookMetadata = require('./metadata/BookMetadata')
|
||||||
const PodcastMetadata = require('./metadata/PodcastMetadata')
|
const PodcastMetadata = require('./metadata/PodcastMetadata')
|
||||||
const DeviceInfo = require('./DeviceInfo')
|
const DeviceInfo = require('./DeviceInfo')
|
||||||
@ -11,6 +12,7 @@ class PlaybackSession {
|
|||||||
this.userId = null
|
this.userId = null
|
||||||
this.libraryId = null
|
this.libraryId = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
|
this.bookId = null
|
||||||
this.episodeId = null
|
this.episodeId = null
|
||||||
|
|
||||||
this.mediaType = null
|
this.mediaType = null
|
||||||
@ -24,6 +26,7 @@ class PlaybackSession {
|
|||||||
this.playMethod = null
|
this.playMethod = null
|
||||||
this.mediaPlayer = null
|
this.mediaPlayer = null
|
||||||
this.deviceInfo = null
|
this.deviceInfo = null
|
||||||
|
this.serverVersion = null
|
||||||
|
|
||||||
this.date = null
|
this.date = null
|
||||||
this.dayOfWeek = null
|
this.dayOfWeek = null
|
||||||
@ -52,6 +55,7 @@ class PlaybackSession {
|
|||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
|
bookId: this.bookId,
|
||||||
episodeId: this.episodeId,
|
episodeId: this.episodeId,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||||
@ -63,6 +67,7 @@ class PlaybackSession {
|
|||||||
playMethod: this.playMethod,
|
playMethod: this.playMethod,
|
||||||
mediaPlayer: this.mediaPlayer,
|
mediaPlayer: this.mediaPlayer,
|
||||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||||
|
serverVersion: this.serverVersion,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
dayOfWeek: this.dayOfWeek,
|
dayOfWeek: this.dayOfWeek,
|
||||||
timeListening: this.timeListening,
|
timeListening: this.timeListening,
|
||||||
@ -79,6 +84,7 @@ class PlaybackSession {
|
|||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
|
bookId: this.bookId,
|
||||||
episodeId: this.episodeId,
|
episodeId: this.episodeId,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||||
@ -90,6 +96,7 @@ class PlaybackSession {
|
|||||||
playMethod: this.playMethod,
|
playMethod: this.playMethod,
|
||||||
mediaPlayer: this.mediaPlayer,
|
mediaPlayer: this.mediaPlayer,
|
||||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||||
|
serverVersion: this.serverVersion,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
dayOfWeek: this.dayOfWeek,
|
dayOfWeek: this.dayOfWeek,
|
||||||
timeListening: this.timeListening,
|
timeListening: this.timeListening,
|
||||||
@ -108,12 +115,20 @@ class PlaybackSession {
|
|||||||
this.userId = session.userId
|
this.userId = session.userId
|
||||||
this.libraryId = session.libraryId || null
|
this.libraryId = session.libraryId || null
|
||||||
this.libraryItemId = session.libraryItemId
|
this.libraryItemId = session.libraryItemId
|
||||||
|
this.bookId = session.bookId
|
||||||
this.episodeId = session.episodeId
|
this.episodeId = session.episodeId
|
||||||
this.mediaType = session.mediaType
|
this.mediaType = session.mediaType
|
||||||
this.duration = session.duration
|
this.duration = session.duration
|
||||||
this.playMethod = session.playMethod
|
this.playMethod = session.playMethod
|
||||||
this.mediaPlayer = session.mediaPlayer || null
|
this.mediaPlayer = session.mediaPlayer || null
|
||||||
|
|
||||||
|
if (session.deviceInfo instanceof DeviceInfo) {
|
||||||
|
this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON())
|
||||||
|
} else {
|
||||||
this.deviceInfo = new DeviceInfo(session.deviceInfo)
|
this.deviceInfo = new DeviceInfo(session.deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.serverVersion = session.serverVersion
|
||||||
this.chapters = session.chapters || []
|
this.chapters = session.chapters || []
|
||||||
|
|
||||||
this.mediaMetadata = null
|
this.mediaMetadata = null
|
||||||
@ -151,7 +166,7 @@ class PlaybackSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get deviceId() {
|
get deviceId() {
|
||||||
return this.deviceInfo?.deviceId
|
return this.deviceInfo?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
get deviceDescription() {
|
get deviceDescription() {
|
||||||
@ -169,10 +184,11 @@ class PlaybackSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
|
setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
|
||||||
this.id = getId('play')
|
this.id = uuidv4()
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.libraryId = libraryItem.libraryId
|
this.libraryId = libraryItem.libraryId
|
||||||
this.libraryItemId = libraryItem.id
|
this.libraryItemId = libraryItem.id
|
||||||
|
this.bookId = episodeId ? null : libraryItem.media.id
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.mediaType = libraryItem.mediaType
|
this.mediaType = libraryItem.mediaType
|
||||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||||
@ -189,6 +205,7 @@ class PlaybackSession {
|
|||||||
|
|
||||||
this.mediaPlayer = mediaPlayer
|
this.mediaPlayer = mediaPlayer
|
||||||
this.deviceInfo = deviceInfo || new DeviceInfo()
|
this.deviceInfo = deviceInfo || new DeviceInfo()
|
||||||
|
this.serverVersion = serverVersion
|
||||||
|
|
||||||
this.timeListening = 0
|
this.timeListening = 0
|
||||||
this.startTime = startTime
|
this.startTime = startTime
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const Logger = require('../Logger')
|
const uuidv4 = require("uuid").v4
|
||||||
const { getId } = require('../utils/index')
|
|
||||||
|
|
||||||
class Playlist {
|
class Playlist {
|
||||||
constructor(playlist) {
|
constructor(playlist) {
|
||||||
@ -88,7 +87,7 @@ class Playlist {
|
|||||||
if (!data.userId || !data.libraryId || !data.name) {
|
if (!data.userId || !data.libraryId || !data.name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
this.id = getId('pl')
|
this.id = uuidv4()
|
||||||
this.userId = data.userId
|
this.userId = data.userId
|
||||||
this.libraryId = data.libraryId
|
this.libraryId = data.libraryId
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { getId } = require('../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
const { sanitizeFilename } = require('../utils/fileUtils')
|
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||||
const globals = require('../utils/globals')
|
const globals = require('../utils/globals')
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ class PodcastEpisodeDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
|
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
|
||||||
this.id = getId('epdl')
|
this.id = uuidv4()
|
||||||
this.podcastEpisode = podcastEpisode
|
this.podcastEpisode = podcastEpisode
|
||||||
|
|
||||||
const url = podcastEpisode.enclosure.url
|
const url = podcastEpisode.enclosure.url
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { getId } = require('../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class Task {
|
class Task {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -35,7 +35,7 @@ class Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(action, title, description, showSuccess, data = {}) {
|
setData(action, title, description, showSuccess, data = {}) {
|
||||||
this.id = getId(action)
|
this.id = uuidv4()
|
||||||
this.action = action
|
this.action = action
|
||||||
this.data = { ...data }
|
this.data = { ...data }
|
||||||
this.title = title
|
this.title = title
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { getId } = require('../../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString')
|
const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString')
|
||||||
|
|
||||||
class Author {
|
class Author {
|
||||||
@ -53,7 +53,7 @@ class Author {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
this.id = getId('aut')
|
this.id = uuidv4()
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.description = data.description || null
|
this.description = data.description || null
|
||||||
this.asin = data.asin || null
|
this.asin = data.asin || null
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const AudioTrack = require('../files/AudioTrack')
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
|
|
||||||
class PodcastEpisode {
|
class PodcastEpisode {
|
||||||
constructor(episode) {
|
constructor(episode) {
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
|
this.podcastId = null
|
||||||
this.id = null
|
this.id = null
|
||||||
this.index = null
|
this.index = null
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ class PodcastEpisode {
|
|||||||
|
|
||||||
construct(episode) {
|
construct(episode) {
|
||||||
this.libraryItemId = episode.libraryItemId
|
this.libraryItemId = episode.libraryItemId
|
||||||
|
this.podcastId = episode.podcastId
|
||||||
this.id = episode.id
|
this.id = episode.id
|
||||||
this.index = episode.index
|
this.index = episode.index
|
||||||
this.season = episode.season
|
this.season = episode.season
|
||||||
@ -54,6 +57,7 @@ class PodcastEpisode {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
|
podcastId: this.podcastId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
season: this.season,
|
season: this.season,
|
||||||
@ -75,6 +79,7 @@ class PodcastEpisode {
|
|||||||
toJSONExpanded() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
|
podcastId: this.podcastId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
season: this.season,
|
season: this.season,
|
||||||
@ -117,7 +122,7 @@ class PodcastEpisode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data, index = 1) {
|
setData(data, index = 1) {
|
||||||
this.id = getId('ep')
|
this.id = uuidv4()
|
||||||
this.index = index
|
this.index = index
|
||||||
this.title = data.title
|
this.title = data.title
|
||||||
this.subtitle = data.subtitle || ''
|
this.subtitle = data.subtitle || ''
|
||||||
@ -133,7 +138,7 @@ class PodcastEpisode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDataFromAudioFile(audioFile, index) {
|
setDataFromAudioFile(audioFile, index) {
|
||||||
this.id = getId('ep')
|
this.id = uuidv4()
|
||||||
this.audioFile = audioFile
|
this.audioFile = audioFile
|
||||||
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
||||||
this.index = index
|
this.index = index
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { getId } = require('../../utils/index')
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class Series {
|
class Series {
|
||||||
constructor(series) {
|
constructor(series) {
|
||||||
@ -40,7 +40,7 @@ class Series {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
this.id = getId('ser')
|
this.id = uuidv4()
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.description = data.description || null
|
this.description = data.description || null
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
|
@ -12,6 +12,7 @@ const EBookFile = require('../files/EBookFile')
|
|||||||
|
|
||||||
class Book {
|
class Book {
|
||||||
constructor(book) {
|
constructor(book) {
|
||||||
|
this.id = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.metadata = null
|
this.metadata = null
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
construct(book) {
|
construct(book) {
|
||||||
|
this.id = book.id
|
||||||
this.libraryItemId = book.libraryItemId
|
this.libraryItemId = book.libraryItemId
|
||||||
this.metadata = new BookMetadata(book.metadata)
|
this.metadata = new BookMetadata(book.metadata)
|
||||||
this.coverPath = book.coverPath
|
this.coverPath = book.coverPath
|
||||||
@ -46,6 +48,7 @@ class Book {
|
|||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
@ -59,6 +62,7 @@ class Book {
|
|||||||
|
|
||||||
toJSONMinified() {
|
toJSONMinified() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
metadata: this.metadata.toJSONMinified(),
|
metadata: this.metadata.toJSONMinified(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
@ -75,6 +79,7 @@ class Book {
|
|||||||
|
|
||||||
toJSONExpanded() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
metadata: this.metadata.toJSONExpanded(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
|
@ -11,6 +11,7 @@ const naturalSort = createNewSortInstance({
|
|||||||
|
|
||||||
class Podcast {
|
class Podcast {
|
||||||
constructor(podcast) {
|
constructor(podcast) {
|
||||||
|
this.id = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.metadata = null
|
this.metadata = null
|
||||||
this.coverPath = null
|
this.coverPath = null
|
||||||
@ -32,6 +33,7 @@ class Podcast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
construct(podcast) {
|
construct(podcast) {
|
||||||
|
this.id = podcast.id
|
||||||
this.libraryItemId = podcast.libraryItemId
|
this.libraryItemId = podcast.libraryItemId
|
||||||
this.metadata = new PodcastMetadata(podcast.metadata)
|
this.metadata = new PodcastMetadata(podcast.metadata)
|
||||||
this.coverPath = podcast.coverPath
|
this.coverPath = podcast.coverPath
|
||||||
@ -50,6 +52,7 @@ class Podcast {
|
|||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
@ -65,6 +68,7 @@ class Podcast {
|
|||||||
|
|
||||||
toJSONMinified() {
|
toJSONMinified() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
metadata: this.metadata.toJSONMinified(),
|
metadata: this.metadata.toJSONMinified(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
@ -80,6 +84,7 @@ class Podcast {
|
|||||||
|
|
||||||
toJSONExpanded() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
metadata: this.metadata.toJSONExpanded(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
@ -284,8 +289,9 @@ class Podcast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addNewEpisodeFromAudioFile(audioFile, index) {
|
addNewEpisodeFromAudioFile(audioFile, index) {
|
||||||
var pe = new PodcastEpisode()
|
const pe = new PodcastEpisode()
|
||||||
pe.libraryItemId = this.libraryItemId
|
pe.libraryItemId = this.libraryItemId
|
||||||
|
pe.podcastId = this.id
|
||||||
audioFile.index = 1 // Only 1 audio file per episode
|
audioFile.index = 1 // Only 1 audio file per episode
|
||||||
pe.setDataFromAudioFile(audioFile, index)
|
pe.setDataFromAudioFile(audioFile, index)
|
||||||
this.episodes.push(pe)
|
this.episodes.push(pe)
|
||||||
|
@ -218,7 +218,7 @@ class BookMetadata {
|
|||||||
|
|
||||||
// Updates author name
|
// Updates author name
|
||||||
updateAuthor(updatedAuthor) {
|
updateAuthor(updatedAuthor) {
|
||||||
var author = this.authors.find(au => au.id === updatedAuthor.id)
|
const author = this.authors.find(au => au.id === updatedAuthor.id)
|
||||||
if (!author || author.name == updatedAuthor.name) return false
|
if (!author || author.name == updatedAuthor.name) return false
|
||||||
author.name = updatedAuthor.name
|
author.name = updatedAuthor.name
|
||||||
return true
|
return true
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
|
|
||||||
class MediaProgress {
|
class MediaProgress {
|
||||||
constructor(progress) {
|
constructor(progress) {
|
||||||
this.id = null
|
this.id = null
|
||||||
|
this.userId = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.episodeId = null // For podcasts
|
this.episodeId = null // For podcasts
|
||||||
|
|
||||||
|
this.mediaItemId = null // For use in new data model
|
||||||
|
this.mediaItemType = null // For use in new data model
|
||||||
|
|
||||||
this.duration = null
|
this.duration = null
|
||||||
this.progress = null // 0 to 1
|
this.progress = null // 0 to 1
|
||||||
this.currentTime = null // seconds
|
this.currentTime = null // seconds
|
||||||
@ -25,8 +31,11 @@ class MediaProgress {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
userId: this.userId,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
episodeId: this.episodeId,
|
episodeId: this.episodeId,
|
||||||
|
mediaItemId: this.mediaItemId,
|
||||||
|
mediaItemType: this.mediaItemType,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
progress: this.progress,
|
progress: this.progress,
|
||||||
currentTime: this.currentTime,
|
currentTime: this.currentTime,
|
||||||
@ -42,8 +51,11 @@ class MediaProgress {
|
|||||||
|
|
||||||
construct(progress) {
|
construct(progress) {
|
||||||
this.id = progress.id
|
this.id = progress.id
|
||||||
|
this.userId = progress.userId
|
||||||
this.libraryItemId = progress.libraryItemId
|
this.libraryItemId = progress.libraryItemId
|
||||||
this.episodeId = progress.episodeId
|
this.episodeId = progress.episodeId
|
||||||
|
this.mediaItemId = progress.mediaItemId
|
||||||
|
this.mediaItemType = progress.mediaItemType
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime || 0
|
this.currentTime = progress.currentTime || 0
|
||||||
@ -60,10 +72,16 @@ class MediaProgress {
|
|||||||
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItemId, progress, episodeId = null) {
|
setData(libraryItem, progress, episodeId, userId) {
|
||||||
this.id = episodeId ? `${libraryItemId}-${episodeId}` : libraryItemId
|
this.id = uuidv4()
|
||||||
this.libraryItemId = libraryItemId
|
this.userId = userId
|
||||||
|
this.libraryItemId = libraryItem.id
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
|
|
||||||
|
// PodcastEpisodeId or BookId
|
||||||
|
this.mediaItemId = episodeId || libraryItem.media.id
|
||||||
|
this.mediaItemType = episodeId ? 'podcastEpisode' : 'book'
|
||||||
|
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = Math.min(1, (progress.progress || 0))
|
this.progress = Math.min(1, (progress.progress || 0))
|
||||||
this.currentTime = progress.currentTime || 0
|
this.currentTime = progress.currentTime || 0
|
||||||
|
@ -5,6 +5,7 @@ const MediaProgress = require('./MediaProgress')
|
|||||||
class User {
|
class User {
|
||||||
constructor(user) {
|
constructor(user) {
|
||||||
this.id = null
|
this.id = null
|
||||||
|
this.oldUserId = null // TODO: Temp for keeping old access tokens
|
||||||
this.username = null
|
this.username = null
|
||||||
this.pash = null
|
this.pash = null
|
||||||
this.type = null
|
this.type = null
|
||||||
@ -73,6 +74,7 @@ class User {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldUserId: this.oldUserId,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
pash: this.pash,
|
pash: this.pash,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
@ -93,6 +95,7 @@ class User {
|
|||||||
toJSONForBrowser(hideRootToken = false, minimal = false) {
|
toJSONForBrowser(hideRootToken = false, minimal = false) {
|
||||||
const json = {
|
const json = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldUserId: this.oldUserId,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
token: (this.type === 'root' && hideRootToken) ? '' : this.token,
|
token: (this.type === 'root' && hideRootToken) ? '' : this.token,
|
||||||
@ -126,6 +129,7 @@ class User {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldUserId: this.oldUserId,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
session,
|
session,
|
||||||
@ -137,6 +141,7 @@ class User {
|
|||||||
|
|
||||||
construct(user) {
|
construct(user) {
|
||||||
this.id = user.id
|
this.id = user.id
|
||||||
|
this.oldUserId = user.oldUserId
|
||||||
this.username = user.username
|
this.username = user.username
|
||||||
this.pash = user.pash
|
this.pash = user.pash
|
||||||
this.type = user.type
|
this.type = user.type
|
||||||
@ -320,7 +325,7 @@ class User {
|
|||||||
if (!itemProgress) {
|
if (!itemProgress) {
|
||||||
const newItemProgress = new MediaProgress()
|
const newItemProgress = new MediaProgress()
|
||||||
|
|
||||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id)
|
||||||
this.mediaProgress.push(newItemProgress)
|
this.mediaProgress.push(newItemProgress)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -336,12 +341,6 @@ class User {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMediaProgressForLibraryItem(libraryItemId) {
|
|
||||||
if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false
|
|
||||||
this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCanAccessLibrary(libraryId) {
|
checkCanAccessLibrary(libraryId) {
|
||||||
if (this.permissions.accessAllLibraries) return true
|
if (this.permissions.accessAllLibraries) return true
|
||||||
if (!this.librariesAccessible) return false
|
if (!this.librariesAccessible) return false
|
||||||
|
@ -2,6 +2,7 @@ const express = require('express')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
@ -37,7 +38,6 @@ const Series = require('../objects/entities/Series')
|
|||||||
|
|
||||||
class ApiRouter {
|
class ApiRouter {
|
||||||
constructor(Server) {
|
constructor(Server) {
|
||||||
this.db = Server.db
|
|
||||||
this.auth = Server.auth
|
this.auth = Server.auth
|
||||||
this.scanner = Server.scanner
|
this.scanner = Server.scanner
|
||||||
this.playbackSessionManager = Server.playbackSessionManager
|
this.playbackSessionManager = Server.playbackSessionManager
|
||||||
@ -356,7 +356,7 @@ class ApiRouter {
|
|||||||
const json = user.toJSONForBrowser(hideRootToken)
|
const json = user.toJSONForBrowser(hideRootToken)
|
||||||
|
|
||||||
json.mediaProgress = json.mediaProgress.map(lip => {
|
json.mediaProgress = json.mediaProgress.map(lip => {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId)
|
const libraryItem = Database.libraryItems.find(li => li.id === lip.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
|
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
|
||||||
lip.media = null
|
lip.media = null
|
||||||
@ -381,11 +381,10 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteLibraryItem(libraryItem) {
|
async handleDeleteLibraryItem(libraryItem) {
|
||||||
// Remove libraryItem from users
|
// Remove media progress for this library item from all users
|
||||||
for (let i = 0; i < this.db.users.length; i++) {
|
for (const user of Database.users) {
|
||||||
const user = this.db.users[i]
|
for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) {
|
||||||
if (user.removeMediaProgressForLibraryItem(libraryItem.id)) {
|
await Database.removeMediaProgress(mediaProgress.id)
|
||||||
await this.db.updateEntity('user', user)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,12 +392,12 @@ class ApiRouter {
|
|||||||
|
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
// remove book from collections
|
// remove book from collections
|
||||||
const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
|
const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id))
|
||||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
for (let i = 0; i < collectionsWithBook.length; i++) {
|
||||||
const collection = collectionsWithBook[i]
|
const collection = collectionsWithBook[i]
|
||||||
collection.removeBook(libraryItem.id)
|
collection.removeBook(libraryItem.id)
|
||||||
await this.db.updateEntity('collection', collection)
|
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
|
||||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check remove empty series
|
// Check remove empty series
|
||||||
@ -406,7 +405,7 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove item from playlists
|
// remove item from playlists
|
||||||
const playlistsWithItem = this.db.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
|
const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
|
||||||
for (let i = 0; i < playlistsWithItem.length; i++) {
|
for (let i = 0; i < playlistsWithItem.length; i++) {
|
||||||
const playlist = playlistsWithItem[i]
|
const playlist = playlistsWithItem[i]
|
||||||
playlist.removeItemsForLibraryItem(libraryItem.id)
|
playlist.removeItemsForLibraryItem(libraryItem.id)
|
||||||
@ -414,11 +413,12 @@ class ApiRouter {
|
|||||||
// If playlist is now empty then remove it
|
// If playlist is now empty then remove it
|
||||||
if (!playlist.items.length) {
|
if (!playlist.items.length) {
|
||||||
Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`)
|
Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`)
|
||||||
await this.db.removeEntity('playlist', playlist.id)
|
await Database.removePlaylist(playlist.id)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(this.db.libraryItems))
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems))
|
||||||
} else {
|
} else {
|
||||||
await this.db.updateEntity('playlist', playlist)
|
await Database.updatePlaylist(playlist)
|
||||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(this.db.libraryItems))
|
|
||||||
|
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,7 +436,7 @@ class ApiRouter {
|
|||||||
await fs.remove(itemMetadataPath)
|
await fs.remove(itemMetadataPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.removeLibraryItem(libraryItem.id)
|
await Database.removeLibraryItem(libraryItem.id)
|
||||||
SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,27 +444,27 @@ class ApiRouter {
|
|||||||
if (!seriesToCheck || !seriesToCheck.length) return
|
if (!seriesToCheck || !seriesToCheck.length) return
|
||||||
|
|
||||||
for (const series of seriesToCheck) {
|
for (const series of seriesToCheck) {
|
||||||
const otherLibraryItemsInSeries = this.db.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
||||||
if (!otherLibraryItemsInSeries.length) {
|
if (!otherLibraryItemsInSeries.length) {
|
||||||
// Close open RSS feed for series
|
// Close open RSS feed for series
|
||||||
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
||||||
Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
||||||
await this.db.removeEntity('series', series.id)
|
await Database.removeSeries(series.id)
|
||||||
// TODO: Socket events for series?
|
// TODO: Socket events for series?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserListeningSessionsHelper(userId) {
|
async getUserListeningSessionsHelper(userId) {
|
||||||
const userSessions = await this.db.selectUserSessions(userId)
|
const userSessions = await Database.getPlaybackSessions({ userId })
|
||||||
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllSessionsWithUserData() {
|
async getAllSessionsWithUserData() {
|
||||||
const sessions = await this.db.getAllSessions()
|
const sessions = await Database.getPlaybackSessions()
|
||||||
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
return sessions.map(se => {
|
return sessions.map(se => {
|
||||||
const user = this.db.users.find(u => u.id === se.userId)
|
const user = Database.users.find(u => u.id === se.userId)
|
||||||
return {
|
return {
|
||||||
...se,
|
...se,
|
||||||
user: user ? { id: user.id, username: user.username } : null
|
user: user ? { id: user.id, username: user.username } : null
|
||||||
@ -533,7 +533,7 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
|
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
|
||||||
let author = this.db.authors.find(au => au.checkNameEquals(authorName))
|
let author = Database.authors.find(au => au.checkNameEquals(authorName))
|
||||||
if (!author) {
|
if (!author) {
|
||||||
author = new Author()
|
author = new Author()
|
||||||
author.setData(mediaMetadata.authors[i])
|
author.setData(mediaMetadata.authors[i])
|
||||||
@ -546,7 +546,7 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newAuthors.length) {
|
if (newAuthors.length) {
|
||||||
await this.db.insertEntities('author', newAuthors)
|
await Database.createBulkAuthors(newAuthors)
|
||||||
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -562,7 +562,7 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
|
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
|
||||||
let seriesItem = this.db.series.find(se => se.checkNameEquals(seriesName))
|
let seriesItem = Database.series.find(se => se.checkNameEquals(seriesName))
|
||||||
if (!seriesItem) {
|
if (!seriesItem) {
|
||||||
seriesItem = new Series()
|
seriesItem = new Series()
|
||||||
seriesItem.setData(mediaMetadata.series[i])
|
seriesItem.setData(mediaMetadata.series[i])
|
||||||
@ -575,7 +575,7 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newSeries.length) {
|
if (newSeries.length) {
|
||||||
await this.db.insertEntities('series', newSeries)
|
await Database.createBulkSeries(newSeries)
|
||||||
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,7 @@ const fs = require('../libs/fsExtra')
|
|||||||
|
|
||||||
|
|
||||||
class HlsRouter {
|
class HlsRouter {
|
||||||
constructor(db, auth, playbackSessionManager) {
|
constructor(auth, playbackSessionManager) {
|
||||||
this.db = db
|
|
||||||
this.auth = auth
|
this.auth = auth
|
||||||
this.playbackSessionManager = playbackSessionManager
|
this.playbackSessionManager = playbackSessionManager
|
||||||
|
|
||||||
|
8
server/routes/index.js
Normal file
8
server/routes/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const libraries = require('./libraries')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use('/libraries', libraries)
|
||||||
|
|
||||||
|
module.exports = router
|
7
server/routes/libraries.js
Normal file
7
server/routes/libraries.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// TODO: Add library routes
|
||||||
|
|
||||||
|
module.exports = router
|
@ -1,4 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const date = require('../libs/dateAndTime')
|
const date = require('../libs/dateAndTime')
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ const Logger = require('../Logger')
|
|||||||
const Library = require('../objects/Library')
|
const Library = require('../objects/Library')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const { getId, secondsToTimestamp } = require('../utils/index')
|
const { secondsToTimestamp } = require('../utils/index')
|
||||||
|
|
||||||
class LibraryScan {
|
class LibraryScan {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -84,7 +85,7 @@ class LibraryScan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(library, scanOptions, type = 'scan') {
|
setData(library, scanOptions, type = 'scan') {
|
||||||
this.id = getId('lscan')
|
this.id = uuidv4()
|
||||||
this.type = type
|
this.type = type
|
||||||
this.library = new Library(library.toJSON()) // clone library
|
this.library = new Library(library.toJSON()) // clone library
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ const fs = require('../libs/fsExtra')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Database = require('../Database')
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir')
|
const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir')
|
||||||
@ -22,8 +23,7 @@ const Series = require('../objects/entities/Series')
|
|||||||
const Task = require('../objects/Task')
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(db, coverManager, taskManager) {
|
constructor(coverManager, taskManager) {
|
||||||
this.db = db
|
|
||||||
this.coverManager = coverManager
|
this.coverManager = coverManager
|
||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItemByRequest(libraryItem) {
|
async scanLibraryItemByRequest(libraryItem) {
|
||||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
@ -108,7 +108,7 @@ class Scanner {
|
|||||||
if (checkRes.updated) hasUpdated = true
|
if (checkRes.updated) hasUpdated = true
|
||||||
|
|
||||||
// Sync other files first so that local images are used as cover art
|
// Sync other files first so that local images are used as cover art
|
||||||
if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata, library.settings)) {
|
if (await libraryItem.syncFiles(Database.serverSettings.scannerPreferOpfMetadata, library.settings)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
@ -160,7 +160,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scanOptions = new ScanOptions()
|
const scanOptions = new ScanOptions()
|
||||||
scanOptions.setData(options, this.db.serverSettings)
|
scanOptions.setData(options, Database.serverSettings)
|
||||||
|
|
||||||
const libraryScan = new LibraryScan()
|
const libraryScan = new LibraryScan()
|
||||||
libraryScan.setData(library, scanOptions)
|
libraryScan.setData(library, scanOptions)
|
||||||
@ -212,7 +212,7 @@ class Scanner {
|
|||||||
|
|
||||||
// Remove items with no inode
|
// Remove items with no inode
|
||||||
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
|
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
|
||||||
const libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
const libraryItemsInLibrary = Database.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
||||||
|
|
||||||
const MaxSizePerChunk = 2.5e9
|
const MaxSizePerChunk = 2.5e9
|
||||||
const itemDataToRescanChunks = []
|
const itemDataToRescanChunks = []
|
||||||
@ -333,7 +333,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateLibraryItemChunk(itemsToUpdate) {
|
async updateLibraryItemChunk(itemsToUpdate) {
|
||||||
await this.db.updateLibraryItems(itemsToUpdate)
|
await Database.updateBulkLibraryItems(itemsToUpdate)
|
||||||
SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +351,7 @@ class Scanner {
|
|||||||
|
|
||||||
if (itemsUpdated.length) {
|
if (itemsUpdated.length) {
|
||||||
libraryScan.resultsUpdated += itemsUpdated.length
|
libraryScan.resultsUpdated += itemsUpdated.length
|
||||||
await this.db.updateLibraryItems(itemsUpdated)
|
await Database.updateBulkLibraryItems(itemsUpdated)
|
||||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
libraryScan.resultsAdded += newLibraryItems.length
|
libraryScan.resultsAdded += newLibraryItems.length
|
||||||
await this.db.insertLibraryItems(newLibraryItems)
|
await Database.createBulkLibraryItems(newLibraryItems)
|
||||||
SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,6 +436,7 @@ class Scanner {
|
|||||||
|
|
||||||
const libraryItem = new LibraryItem()
|
const libraryItem = new LibraryItem()
|
||||||
libraryItem.setData(library.mediaType, libraryItemData)
|
libraryItem.setData(library.mediaType, libraryItemData)
|
||||||
|
libraryItem.setLastScan()
|
||||||
|
|
||||||
const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
||||||
if (mediaFiles.length) {
|
if (mediaFiles.length) {
|
||||||
@ -478,7 +479,7 @@ class Scanner {
|
|||||||
if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) {
|
if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) {
|
||||||
var newAuthors = []
|
var newAuthors = []
|
||||||
libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => {
|
libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => {
|
||||||
var _author = this.db.authors.find(au => au.checkNameEquals(tempMinAuthor.name))
|
var _author = Database.authors.find(au => au.checkNameEquals(tempMinAuthor.name))
|
||||||
if (!_author) _author = newAuthors.find(au => au.checkNameEquals(tempMinAuthor.name)) // Check new unsaved authors
|
if (!_author) _author = newAuthors.find(au => au.checkNameEquals(tempMinAuthor.name)) // Check new unsaved authors
|
||||||
if (!_author) { // Must create new author
|
if (!_author) { // Must create new author
|
||||||
_author = new Author()
|
_author = new Author()
|
||||||
@ -492,14 +493,14 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (newAuthors.length) {
|
if (newAuthors.length) {
|
||||||
await this.db.insertEntities('author', newAuthors)
|
await Database.createBulkAuthors(newAuthors)
|
||||||
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) {
|
if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) {
|
||||||
var newSeries = []
|
var newSeries = []
|
||||||
libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => {
|
libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => {
|
||||||
var _series = this.db.series.find(se => se.checkNameEquals(tempMinSeries.name))
|
var _series = Database.series.find(se => se.checkNameEquals(tempMinSeries.name))
|
||||||
if (!_series) _series = newSeries.find(se => se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series
|
if (!_series) _series = newSeries.find(se => se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series
|
||||||
if (!_series) { // Must create new series
|
if (!_series) { // Must create new series
|
||||||
_series = new Series()
|
_series = new Series()
|
||||||
@ -513,7 +514,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (newSeries.length) {
|
if (newSeries.length) {
|
||||||
await this.db.insertEntities('series', newSeries)
|
await Database.createBulkSeries(newSeries)
|
||||||
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,7 +552,7 @@ class Scanner {
|
|||||||
|
|
||||||
for (const folderId in folderGroups) {
|
for (const folderId in folderGroups) {
|
||||||
const libraryId = folderGroups[folderId].libraryId
|
const libraryId = folderGroups[folderId].libraryId
|
||||||
const library = this.db.libraries.find(lib => lib.id === libraryId)
|
const library = Database.libraries.find(lib => lib.id === libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
||||||
continue;
|
continue;
|
||||||
@ -597,12 +598,12 @@ class Scanner {
|
|||||||
const altDir = `${itemDir}/${firstNest}`
|
const altDir = `${itemDir}/${firstNest}`
|
||||||
|
|
||||||
const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir)
|
const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir)
|
||||||
const childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath))
|
const childLibraryItem = Database.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath))
|
||||||
if (!childLibraryItem) {
|
if (!childLibraryItem) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir)
|
const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir)
|
||||||
const altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath))
|
const altChildLibraryItem = Database.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath))
|
||||||
if (altChildLibraryItem) {
|
if (altChildLibraryItem) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -619,9 +620,9 @@ class Scanner {
|
|||||||
const dirIno = await getIno(fullPath)
|
const dirIno = await getIno(fullPath)
|
||||||
|
|
||||||
// Check if book dir group is already an item
|
// Check if book dir group is already an item
|
||||||
let existingLibraryItem = this.db.libraryItems.find(li => fullPath.startsWith(li.path))
|
let existingLibraryItem = Database.libraryItems.find(li => fullPath.startsWith(li.path))
|
||||||
if (!existingLibraryItem) {
|
if (!existingLibraryItem) {
|
||||||
existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno)
|
existingLibraryItem = Database.libraryItems.find(li => li.ino === dirIno)
|
||||||
if (existingLibraryItem) {
|
if (existingLibraryItem) {
|
||||||
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`)
|
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`)
|
||||||
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
|
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
|
||||||
@ -636,7 +637,7 @@ class Scanner {
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`)
|
Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`)
|
||||||
existingLibraryItem.setMissing()
|
existingLibraryItem.setMissing()
|
||||||
await this.db.updateLibraryItem(existingLibraryItem)
|
await Database.updateLibraryItem(existingLibraryItem)
|
||||||
SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded())
|
||||||
|
|
||||||
itemGroupingResults[itemDir] = ScanResult.REMOVED
|
itemGroupingResults[itemDir] = ScanResult.REMOVED
|
||||||
@ -654,7 +655,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a library item is a subdirectory of this dir
|
// Check if a library item is a subdirectory of this dir
|
||||||
var childItem = this.db.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/'))
|
var childItem = Database.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/'))
|
||||||
if (childItem) {
|
if (childItem) {
|
||||||
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
||||||
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
||||||
@ -666,7 +667,7 @@ class Scanner {
|
|||||||
var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem)
|
var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem)
|
||||||
if (newLibraryItem) {
|
if (newLibraryItem) {
|
||||||
await this.createNewAuthorsAndSeries(newLibraryItem)
|
await this.createNewAuthorsAndSeries(newLibraryItem)
|
||||||
await this.db.insertLibraryItem(newLibraryItem)
|
await Database.createLibraryItem(newLibraryItem)
|
||||||
SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
|
itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
|
||||||
@ -686,7 +687,7 @@ class Scanner {
|
|||||||
titleDistance: 2,
|
titleDistance: 2,
|
||||||
authorDistance: 2
|
authorDistance: 2
|
||||||
}
|
}
|
||||||
const scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
|
const scannerCoverProvider = Database.serverSettings.scannerCoverProvider
|
||||||
const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
|
const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
|
||||||
if (results.length) {
|
if (results.length) {
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
|
||||||
@ -716,7 +717,7 @@ class Scanner {
|
|||||||
|
|
||||||
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
|
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
|
||||||
// the overrideDefaults option is not set or set to false.
|
// the overrideDefaults option is not set or set to false.
|
||||||
if ((overrideDefaults == false) && (this.db.serverSettings.scannerPreferMatchedMetadata)) {
|
if ((overrideDefaults == false) && (Database.serverSettings.scannerPreferMatchedMetadata)) {
|
||||||
options.overrideCover = true
|
options.overrideCover = true
|
||||||
options.overrideDetails = true
|
options.overrideDetails = true
|
||||||
}
|
}
|
||||||
@ -783,7 +784,7 @@ class Scanner {
|
|||||||
await this.quickMatchPodcastEpisodes(libraryItem, options)
|
await this.quickMatchPodcastEpisodes(libraryItem, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -878,11 +879,11 @@ class Scanner {
|
|||||||
const authorPayload = []
|
const authorPayload = []
|
||||||
for (let index = 0; index < matchData.author.length; index++) {
|
for (let index = 0; index < matchData.author.length; index++) {
|
||||||
const authorName = matchData.author[index]
|
const authorName = matchData.author[index]
|
||||||
var author = this.db.authors.find(au => au.checkNameEquals(authorName))
|
var author = Database.authors.find(au => au.checkNameEquals(authorName))
|
||||||
if (!author) {
|
if (!author) {
|
||||||
author = new Author()
|
author = new Author()
|
||||||
author.setData({ name: authorName })
|
author.setData({ name: authorName })
|
||||||
await this.db.insertEntity('author', author)
|
await Database.createAuthor(author)
|
||||||
SocketAuthority.emitter('author_added', author.toJSON())
|
SocketAuthority.emitter('author_added', author.toJSON())
|
||||||
}
|
}
|
||||||
authorPayload.push(author.toJSONMinimal())
|
authorPayload.push(author.toJSONMinimal())
|
||||||
@ -896,11 +897,11 @@ class Scanner {
|
|||||||
const seriesPayload = []
|
const seriesPayload = []
|
||||||
for (let index = 0; index < matchData.series.length; index++) {
|
for (let index = 0; index < matchData.series.length; index++) {
|
||||||
const seriesMatchItem = matchData.series[index]
|
const seriesMatchItem = matchData.series[index]
|
||||||
var seriesItem = this.db.series.find(au => au.checkNameEquals(seriesMatchItem.series))
|
var seriesItem = Database.series.find(au => au.checkNameEquals(seriesMatchItem.series))
|
||||||
if (!seriesItem) {
|
if (!seriesItem) {
|
||||||
seriesItem = new Series()
|
seriesItem = new Series()
|
||||||
seriesItem.setData({ name: seriesMatchItem.series })
|
seriesItem.setData({ name: seriesMatchItem.series })
|
||||||
await this.db.insertEntity('series', seriesItem)
|
await Database.createSeries(seriesItem)
|
||||||
SocketAuthority.emitter('series_added', seriesItem.toJSON())
|
SocketAuthority.emitter('series_added', seriesItem.toJSON())
|
||||||
}
|
}
|
||||||
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
|
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
|
||||||
@ -981,7 +982,7 @@ class Scanner {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemsInLibrary = this.db.getLibraryItemsInLibrary(library.id)
|
const itemsInLibrary = Database.libraryItems.filter(li => li.libraryId === library.id)
|
||||||
if (!itemsInLibrary.length) {
|
if (!itemsInLibrary.length) {
|
||||||
Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`)
|
Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`)
|
||||||
return
|
return
|
||||||
|
@ -17,24 +17,31 @@
|
|||||||
@param value2 Other item to compare
|
@param value2 Other item to compare
|
||||||
@param stack Used internally to track circular refs - don't set it
|
@param stack Used internally to track circular refs - don't set it
|
||||||
*/
|
*/
|
||||||
module.exports = function areEquivalent(value1, value2, stack = []) {
|
module.exports = function areEquivalent(value1, value2, numToString = false, stack = []) {
|
||||||
|
if (numToString) {
|
||||||
|
if (value1 !== null && !isNaN(value1)) value1 = String(value1)
|
||||||
|
if (value2 !== null && !isNaN(value2)) value2 = String(value2)
|
||||||
|
}
|
||||||
|
|
||||||
// Numbers, strings, null, undefined, symbols, functions, booleans.
|
// Numbers, strings, null, undefined, symbols, functions, booleans.
|
||||||
// Also: objects (incl. arrays) that are actually the same instance
|
// Also: objects (incl. arrays) that are actually the same instance
|
||||||
if (value1 === value2) {
|
if (value1 === value2) {
|
||||||
// Fast and done
|
// Fast and done
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truthy check to handle value1=null, value2=Object
|
// Truthy check to handle value1=null, value2=Object
|
||||||
if ((value1 && !value2) || (!value1 && value2)) {
|
if ((value1 && !value2) || (!value1 && value2)) {
|
||||||
|
console.log('value1/value2 falsy mismatch', value1, value2)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const type1 = typeof value1;
|
const type1 = typeof value1
|
||||||
|
|
||||||
// Ensure types match
|
// Ensure types match
|
||||||
if (type1 !== typeof value2) {
|
if (type1 !== typeof value2) {
|
||||||
return false;
|
console.log('type diff', type1, typeof value2)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case for number: check for NaN on both sides
|
// Special case for number: check for NaN on both sides
|
||||||
@ -49,26 +56,27 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
|
|||||||
// Failed initial equals test, but could still have equivalent
|
// Failed initial equals test, but could still have equivalent
|
||||||
// implementations - note, will match on functions that have same name
|
// implementations - note, will match on functions that have same name
|
||||||
// and are native code: `function abc() { [native code] }`
|
// and are native code: `function abc() { [native code] }`
|
||||||
return value1.toString() === value2.toString();
|
return value1.toString() === value2.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For these types, cannot still be equal at this point, so fast-fail
|
// For these types, cannot still be equal at this point, so fast-fail
|
||||||
if (type1 === 'bigint' || type1 === 'boolean' ||
|
if (type1 === 'bigint' || type1 === 'boolean' ||
|
||||||
type1 === 'function' || type1 === 'string' ||
|
type1 === 'function' || type1 === 'string' ||
|
||||||
type1 === 'symbol') {
|
type1 === 'symbol') {
|
||||||
return false;
|
console.log('no match for values', value1, value2)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// For dates, cast to number and ensure equal or both NaN (note, if same
|
// For dates, cast to number and ensure equal or both NaN (note, if same
|
||||||
// exact instance then we're not here - that was checked above)
|
// exact instance then we're not here - that was checked above)
|
||||||
if (value1 instanceof Date) {
|
if (value1 instanceof Date) {
|
||||||
if (!(value2 instanceof Date)) {
|
if (!(value2 instanceof Date)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
// Convert to number to compare
|
// Convert to number to compare
|
||||||
const asNum1 = +value1, asNum2 = +value2;
|
const asNum1 = +value1, asNum2 = +value2
|
||||||
// Check if both invalid (NaN) or are same value
|
// Check if both invalid (NaN) or are same value
|
||||||
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2));
|
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, it's a reference type and could be circular, so
|
// At this point, it's a reference type and could be circular, so
|
||||||
@ -80,61 +88,67 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// breadcrumb
|
// breadcrumb
|
||||||
stack.push(value1);
|
stack.push(value1)
|
||||||
|
|
||||||
// Handle arrays
|
// Handle arrays
|
||||||
if (Array.isArray(value1)) {
|
if (Array.isArray(value1)) {
|
||||||
if (!Array.isArray(value2)) {
|
if (!Array.isArray(value2)) {
|
||||||
return false;
|
console.log('value2 is not array but value1 is', value1, value2)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const length = value1.length;
|
const length = value1.length
|
||||||
|
|
||||||
if (length !== value2.length) {
|
if (length !== value2.length) {
|
||||||
return false;
|
console.log('array length diff', length)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
if (!areEquivalent(value1[i], value2[i], stack)) {
|
if (!areEquivalent(value1[i], value2[i], numToString, stack)) {
|
||||||
return false;
|
console.log('2 array items are not equiv', value1[i], value2[i])
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final case: object
|
// Final case: object
|
||||||
|
|
||||||
// get both key lists and check length
|
// get both key lists and check length
|
||||||
const keys1 = Object.keys(value1);
|
const keys1 = Object.keys(value1)
|
||||||
const keys2 = Object.keys(value2);
|
const keys2 = Object.keys(value2)
|
||||||
const numKeys = keys1.length;
|
const numKeys = keys1.length
|
||||||
|
|
||||||
if (keys2.length !== numKeys) {
|
if (keys2.length !== numKeys) {
|
||||||
return false;
|
console.log('Key length is diff', keys2.length, numKeys)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty object on both sides?
|
// Empty object on both sides?
|
||||||
if (numKeys === 0) {
|
if (numKeys === 0) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort is a native call so it's very fast - much faster than comparing the
|
// sort is a native call so it's very fast - much faster than comparing the
|
||||||
// values at each key if it can be avoided, so do the sort and then
|
// values at each key if it can be avoided, so do the sort and then
|
||||||
// ensure every key matches at every index
|
// ensure every key matches at every index
|
||||||
keys1.sort();
|
keys1.sort()
|
||||||
keys2.sort();
|
keys2.sort()
|
||||||
|
|
||||||
// Ensure perfect match across all keys
|
// Ensure perfect match across all keys
|
||||||
for (let i = 0; i < numKeys; i++) {
|
for (let i = 0; i < numKeys; i++) {
|
||||||
if (keys1[i] !== keys2[i]) {
|
if (keys1[i] !== keys2[i]) {
|
||||||
return false;
|
console.log('object key is not equiv', keys1[i], keys2[i])
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure perfect match across all values
|
// Ensure perfect match across all values
|
||||||
for (let i = 0; i < numKeys; i++) {
|
for (let i = 0; i < numKeys; i++) {
|
||||||
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) {
|
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) {
|
||||||
return false;
|
console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]])
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const Database = require('../Database')
|
||||||
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
@ -574,7 +575,7 @@ module.exports = {
|
|||||||
const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id)
|
const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id)
|
||||||
|
|
||||||
if (!seriesMap[librarySeries.id]) {
|
if (!seriesMap[librarySeries.id]) {
|
||||||
const seriesObj = ctx.db.series.find(se => se.id === librarySeries.id)
|
const seriesObj = Database.series.find(se => se.id === librarySeries.id)
|
||||||
if (seriesObj) {
|
if (seriesObj) {
|
||||||
const series = {
|
const series = {
|
||||||
...seriesObj.toJSON(),
|
...seriesObj.toJSON(),
|
||||||
@ -626,7 +627,7 @@ module.exports = {
|
|||||||
if (libraryItem.media.metadata.authors.length) {
|
if (libraryItem.media.metadata.authors.length) {
|
||||||
for (const libraryAuthor of libraryItem.media.metadata.authors) {
|
for (const libraryAuthor of libraryItem.media.metadata.authors) {
|
||||||
if (!authorMap[libraryAuthor.id]) {
|
if (!authorMap[libraryAuthor.id]) {
|
||||||
const authorObj = ctx.db.authors.find(au => au.id === libraryAuthor.id)
|
const authorObj = Database.authors.find(au => au.id === libraryAuthor.id)
|
||||||
if (authorObj) {
|
if (authorObj) {
|
||||||
const author = {
|
const author = {
|
||||||
...authorObj.toJSON(),
|
...authorObj.toJSON(),
|
||||||
|
763
server/utils/migrations/dbMigration.js
Normal file
763
server/utils/migrations/dbMigration.js
Normal file
@ -0,0 +1,763 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const uuidv4 = require("uuid").v4
|
||||||
|
const Logger = require('../../Logger')
|
||||||
|
const fs = require('../../libs/fsExtra')
|
||||||
|
const oldDbFiles = require('./oldDbFiles')
|
||||||
|
|
||||||
|
const oldDbIdMap = {
|
||||||
|
users: {},
|
||||||
|
libraries: {},
|
||||||
|
libraryFolders: {},
|
||||||
|
libraryItems: {},
|
||||||
|
authors: {},
|
||||||
|
series: {},
|
||||||
|
collections: {},
|
||||||
|
podcastEpisodes: {},
|
||||||
|
books: {}, // key is library item id
|
||||||
|
podcasts: {}, // key is library item id
|
||||||
|
devices: {} // key is a json stringify of the old DeviceInfo data OR deviceId if it exists
|
||||||
|
}
|
||||||
|
const newRecords = {
|
||||||
|
user: [],
|
||||||
|
library: [],
|
||||||
|
libraryFolder: [],
|
||||||
|
author: [],
|
||||||
|
book: [],
|
||||||
|
podcast: [],
|
||||||
|
libraryItem: [],
|
||||||
|
bookAuthor: [],
|
||||||
|
series: [],
|
||||||
|
bookSeries: [],
|
||||||
|
podcastEpisode: [],
|
||||||
|
mediaProgress: [],
|
||||||
|
device: [],
|
||||||
|
playbackSession: [],
|
||||||
|
collection: [],
|
||||||
|
collectionBook: [],
|
||||||
|
playlist: [],
|
||||||
|
playlistMediaItem: [],
|
||||||
|
feed: [],
|
||||||
|
feedEpisode: [],
|
||||||
|
setting: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeviceInfoString(deviceInfo, UserId) {
|
||||||
|
if (!deviceInfo) return null
|
||||||
|
if (deviceInfo.deviceId) return deviceInfo.deviceId
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
UserId,
|
||||||
|
deviceInfo.browserName || null,
|
||||||
|
deviceInfo.browserVersion || null,
|
||||||
|
deviceInfo.osName || null,
|
||||||
|
deviceInfo.osVersion || null,
|
||||||
|
deviceInfo.clientVersion || null,
|
||||||
|
deviceInfo.manufacturer || null,
|
||||||
|
deviceInfo.model || null,
|
||||||
|
deviceInfo.sdkVersion || null,
|
||||||
|
deviceInfo.ipAddress || null
|
||||||
|
].map(k => k || '')
|
||||||
|
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateBook(oldLibraryItem, LibraryItem) {
|
||||||
|
const oldBook = oldLibraryItem.media
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate Book
|
||||||
|
//
|
||||||
|
const Book = {
|
||||||
|
id: uuidv4(),
|
||||||
|
title: oldBook.metadata.title,
|
||||||
|
subtitle: oldBook.metadata.subtitle,
|
||||||
|
publishedYear: oldBook.metadata.publishedYear,
|
||||||
|
publishedDate: oldBook.metadata.publishedDate,
|
||||||
|
publisher: oldBook.metadata.publisher,
|
||||||
|
description: oldBook.metadata.description,
|
||||||
|
isbn: oldBook.metadata.isbn,
|
||||||
|
asin: oldBook.metadata.asin,
|
||||||
|
language: oldBook.metadata.language,
|
||||||
|
explicit: !!oldBook.metadata.explicit,
|
||||||
|
abridged: !!oldBook.metadata.abridged,
|
||||||
|
lastCoverSearchQuery: oldBook.lastCoverSearchQuery,
|
||||||
|
lastCoverSearch: oldBook.lastCoverSearch,
|
||||||
|
createdAt: LibraryItem.createdAt,
|
||||||
|
updatedAt: LibraryItem.updatedAt,
|
||||||
|
narrators: oldBook.metadata.narrators,
|
||||||
|
ebookFile: oldBook.ebookFile,
|
||||||
|
coverPath: oldBook.coverPath,
|
||||||
|
audioFiles: oldBook.audioFiles,
|
||||||
|
chapters: oldBook.chapters,
|
||||||
|
tags: oldBook.tags,
|
||||||
|
genres: oldBook.metadata.genres
|
||||||
|
}
|
||||||
|
newRecords.book.push(Book)
|
||||||
|
oldDbIdMap.books[oldLibraryItem.id] = Book.id
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate BookAuthors
|
||||||
|
//
|
||||||
|
for (const oldBookAuthor of oldBook.metadata.authors) {
|
||||||
|
if (oldDbIdMap.authors[oldBookAuthor.id]) {
|
||||||
|
newRecords.bookAuthor.push({
|
||||||
|
id: uuidv4(),
|
||||||
|
authorId: oldDbIdMap.authors[oldBookAuthor.id],
|
||||||
|
bookId: Book.id
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[dbMigration] migrateBook: Book author not found "${oldBookAuthor.name}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate BookSeries
|
||||||
|
//
|
||||||
|
for (const oldBookSeries of oldBook.metadata.series) {
|
||||||
|
if (oldDbIdMap.series[oldBookSeries.id]) {
|
||||||
|
const BookSeries = {
|
||||||
|
id: uuidv4(),
|
||||||
|
sequence: oldBookSeries.sequence,
|
||||||
|
seriesId: oldDbIdMap.series[oldBookSeries.id],
|
||||||
|
bookId: Book.id
|
||||||
|
}
|
||||||
|
newRecords.bookSeries.push(BookSeries)
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migratePodcast(oldLibraryItem, LibraryItem) {
|
||||||
|
const oldPodcast = oldLibraryItem.media
|
||||||
|
const oldPodcastMetadata = oldPodcast.metadata
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate Podcast
|
||||||
|
//
|
||||||
|
const Podcast = {
|
||||||
|
id: uuidv4(),
|
||||||
|
title: oldPodcastMetadata.title,
|
||||||
|
author: oldPodcastMetadata.author,
|
||||||
|
releaseDate: oldPodcastMetadata.releaseDate,
|
||||||
|
feedURL: oldPodcastMetadata.feedUrl,
|
||||||
|
imageURL: oldPodcastMetadata.imageUrl,
|
||||||
|
description: oldPodcastMetadata.description,
|
||||||
|
itunesPageURL: oldPodcastMetadata.itunesPageUrl,
|
||||||
|
itunesId: oldPodcastMetadata.itunesId,
|
||||||
|
itunesArtistId: oldPodcastMetadata.itunesArtistId,
|
||||||
|
language: oldPodcastMetadata.language,
|
||||||
|
podcastType: oldPodcastMetadata.type,
|
||||||
|
explicit: !!oldPodcastMetadata.explicit,
|
||||||
|
autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
|
||||||
|
autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
|
||||||
|
lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
|
||||||
|
maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
|
||||||
|
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
|
||||||
|
lastCoverSearchQuery: oldPodcast.lastCoverSearchQuery,
|
||||||
|
lastCoverSearch: oldPodcast.lastCoverSearch,
|
||||||
|
createdAt: LibraryItem.createdAt,
|
||||||
|
updatedAt: LibraryItem.updatedAt,
|
||||||
|
coverPath: oldPodcast.coverPath,
|
||||||
|
tags: oldPodcast.tags,
|
||||||
|
genres: oldPodcastMetadata.genres
|
||||||
|
}
|
||||||
|
newRecords.podcast.push(Podcast)
|
||||||
|
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate PodcastEpisodes
|
||||||
|
//
|
||||||
|
const oldEpisodes = oldPodcast.episodes || []
|
||||||
|
for (const oldEpisode of oldEpisodes) {
|
||||||
|
const PodcastEpisode = {
|
||||||
|
id: uuidv4(),
|
||||||
|
index: oldEpisode.index,
|
||||||
|
season: oldEpisode.season,
|
||||||
|
episode: oldEpisode.episode,
|
||||||
|
episodeType: oldEpisode.episodeType,
|
||||||
|
title: oldEpisode.title,
|
||||||
|
subtitle: oldEpisode.subtitle,
|
||||||
|
description: oldEpisode.description,
|
||||||
|
pubDate: oldEpisode.pubDate,
|
||||||
|
enclosureURL: oldEpisode.enclosure?.url || null,
|
||||||
|
enclosureSize: oldEpisode.enclosure?.length || null,
|
||||||
|
enclosureType: oldEpisode.enclosure?.type || null,
|
||||||
|
publishedAt: oldEpisode.publishedAt,
|
||||||
|
createdAt: oldEpisode.addedAt,
|
||||||
|
updatedAt: oldEpisode.updatedAt,
|
||||||
|
podcastId: Podcast.id,
|
||||||
|
audioFile: oldEpisode.audioFile,
|
||||||
|
chapters: oldEpisode.chapters
|
||||||
|
}
|
||||||
|
newRecords.podcastEpisode.push(PodcastEpisode)
|
||||||
|
oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateLibraryItems(oldLibraryItems) {
|
||||||
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
|
const libraryFolderId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId]
|
||||||
|
if (!libraryFolderId) {
|
||||||
|
Logger.error(`[dbMigration] migrateLibraryItems: Old library folder id not found "${oldLibraryItem.folderId}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const libraryId = oldDbIdMap.libraries[oldLibraryItem.libraryId]
|
||||||
|
if (!libraryId) {
|
||||||
|
Logger.error(`[dbMigration] migrateLibraryItems: Old library id not found "${oldLibraryItem.libraryId}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!['book', 'podcast'].includes(oldLibraryItem.mediaType)) {
|
||||||
|
Logger.error(`[dbMigration] migrateLibraryItems: Not migrating library item with mediaType=${oldLibraryItem.mediaType}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate LibraryItem
|
||||||
|
//
|
||||||
|
const LibraryItem = {
|
||||||
|
id: uuidv4(),
|
||||||
|
ino: oldLibraryItem.ino,
|
||||||
|
path: oldLibraryItem.path,
|
||||||
|
relPath: oldLibraryItem.relPath,
|
||||||
|
mediaId: null, // set below
|
||||||
|
mediaType: oldLibraryItem.mediaType,
|
||||||
|
isFile: !!oldLibraryItem.isFile,
|
||||||
|
isMissing: !!oldLibraryItem.isMissing,
|
||||||
|
isInvalid: !!oldLibraryItem.isInvalid,
|
||||||
|
mtime: oldLibraryItem.mtimeMs,
|
||||||
|
ctime: oldLibraryItem.ctimeMs,
|
||||||
|
birthtime: oldLibraryItem.birthtimeMs,
|
||||||
|
lastScan: oldLibraryItem.lastScan,
|
||||||
|
lastScanVersion: oldLibraryItem.scanVersion,
|
||||||
|
createdAt: oldLibraryItem.addedAt,
|
||||||
|
updatedAt: oldLibraryItem.updatedAt,
|
||||||
|
libraryId,
|
||||||
|
libraryFolderId,
|
||||||
|
libraryFiles: oldLibraryItem.libraryFiles.map(lf => {
|
||||||
|
if (lf.isSupplementary === undefined) lf.isSupplementary = null
|
||||||
|
return lf
|
||||||
|
})
|
||||||
|
}
|
||||||
|
oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id
|
||||||
|
newRecords.libraryItem.push(LibraryItem)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate Book/Podcast
|
||||||
|
//
|
||||||
|
if (oldLibraryItem.mediaType === 'book') {
|
||||||
|
migrateBook(oldLibraryItem, LibraryItem)
|
||||||
|
LibraryItem.mediaId = oldDbIdMap.books[oldLibraryItem.id]
|
||||||
|
} else if (oldLibraryItem.mediaType === 'podcast') {
|
||||||
|
migratePodcast(oldLibraryItem, LibraryItem)
|
||||||
|
LibraryItem.mediaId = oldDbIdMap.podcasts[oldLibraryItem.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateLibraries(oldLibraries) {
|
||||||
|
for (const oldLibrary of oldLibraries) {
|
||||||
|
if (!['book', 'podcast'].includes(oldLibrary.mediaType)) {
|
||||||
|
Logger.error(`[dbMigration] migrateLibraries: Not migrating library with mediaType=${oldLibrary.mediaType}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate Library
|
||||||
|
//
|
||||||
|
const Library = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: oldLibrary.name,
|
||||||
|
displayOrder: oldLibrary.displayOrder,
|
||||||
|
icon: oldLibrary.icon || null,
|
||||||
|
mediaType: oldLibrary.mediaType || null,
|
||||||
|
provider: oldLibrary.provider,
|
||||||
|
settings: oldLibrary.settings || {},
|
||||||
|
createdAt: oldLibrary.createdAt,
|
||||||
|
updatedAt: oldLibrary.lastUpdate
|
||||||
|
}
|
||||||
|
oldDbIdMap.libraries[oldLibrary.id] = Library.id
|
||||||
|
newRecords.library.push(Library)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate LibraryFolders
|
||||||
|
//
|
||||||
|
for (const oldFolder of oldLibrary.folders) {
|
||||||
|
const LibraryFolder = {
|
||||||
|
id: uuidv4(),
|
||||||
|
path: oldFolder.fullPath,
|
||||||
|
createdAt: oldFolder.addedAt,
|
||||||
|
updatedAt: oldLibrary.lastUpdate,
|
||||||
|
libraryId: Library.id
|
||||||
|
}
|
||||||
|
oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id
|
||||||
|
newRecords.libraryFolder.push(LibraryFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateAuthors(oldAuthors) {
|
||||||
|
for (const oldAuthor of oldAuthors) {
|
||||||
|
const Author = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: oldAuthor.name,
|
||||||
|
asin: oldAuthor.asin || null,
|
||||||
|
description: oldAuthor.description,
|
||||||
|
imagePath: oldAuthor.imagePath,
|
||||||
|
createdAt: oldAuthor.addedAt || Date.now(),
|
||||||
|
updatedAt: oldAuthor.updatedAt || Date.now()
|
||||||
|
}
|
||||||
|
oldDbIdMap.authors[oldAuthor.id] = Author.id
|
||||||
|
newRecords.author.push(Author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSeries(oldSerieses) {
|
||||||
|
for (const oldSeries of oldSerieses) {
|
||||||
|
const Series = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: oldSeries.name,
|
||||||
|
description: oldSeries.description || null,
|
||||||
|
createdAt: oldSeries.addedAt || Date.now(),
|
||||||
|
updatedAt: oldSeries.updatedAt || Date.now()
|
||||||
|
}
|
||||||
|
oldDbIdMap.series[oldSeries.id] = Series.id
|
||||||
|
newRecords.series.push(Series)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateUsers(oldUsers) {
|
||||||
|
for (const oldUser of oldUsers) {
|
||||||
|
//
|
||||||
|
// Migrate User
|
||||||
|
//
|
||||||
|
const User = {
|
||||||
|
id: uuidv4(),
|
||||||
|
username: oldUser.username,
|
||||||
|
pash: oldUser.pash || null,
|
||||||
|
type: oldUser.type || null,
|
||||||
|
token: oldUser.token || null,
|
||||||
|
isActive: !!oldUser.isActive,
|
||||||
|
lastSeen: oldUser.lastSeen || null,
|
||||||
|
extraData: {
|
||||||
|
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||||
|
oldUserId: oldUser.id // Used to keep old tokens
|
||||||
|
},
|
||||||
|
createdAt: oldUser.createdAt || Date.now(),
|
||||||
|
permissions: {
|
||||||
|
...oldUser.permissions,
|
||||||
|
librariesAccessible: oldUser.librariesAccessible || [],
|
||||||
|
itemTagsSelected: oldUser.itemTagsSelected || []
|
||||||
|
},
|
||||||
|
bookmarks: oldUser.bookmarks
|
||||||
|
}
|
||||||
|
oldDbIdMap.users[oldUser.id] = User.id
|
||||||
|
newRecords.user.push(User)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate MediaProgress
|
||||||
|
//
|
||||||
|
for (const oldMediaProgress of oldUser.mediaProgress) {
|
||||||
|
let mediaItemType = 'book'
|
||||||
|
let mediaItemId = null
|
||||||
|
if (oldMediaProgress.episodeId) {
|
||||||
|
mediaItemType = 'podcastEpisode'
|
||||||
|
mediaItemId = oldDbIdMap.podcastEpisodes[oldMediaProgress.episodeId]
|
||||||
|
} else {
|
||||||
|
mediaItemId = oldDbIdMap.books[oldMediaProgress.libraryItemId]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mediaItemId) {
|
||||||
|
Logger.warn(`[dbMigration] migrateUsers: Unable to find media item for media progress "${oldMediaProgress.id}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaProgress = {
|
||||||
|
id: uuidv4(),
|
||||||
|
mediaItemId,
|
||||||
|
mediaItemType,
|
||||||
|
duration: oldMediaProgress.duration,
|
||||||
|
currentTime: oldMediaProgress.currentTime,
|
||||||
|
ebookLocation: oldMediaProgress.ebookLocation || null,
|
||||||
|
ebookProgress: oldMediaProgress.ebookProgress || null,
|
||||||
|
isFinished: !!oldMediaProgress.isFinished,
|
||||||
|
hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening,
|
||||||
|
finishedAt: oldMediaProgress.finishedAt,
|
||||||
|
createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate,
|
||||||
|
updatedAt: oldMediaProgress.lastUpdate,
|
||||||
|
userId: User.id,
|
||||||
|
extraData: {
|
||||||
|
libraryItemId: oldDbIdMap.libraryItems[oldMediaProgress.libraryItemId],
|
||||||
|
progress: oldMediaProgress.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRecords.mediaProgress.push(MediaProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSessions(oldSessions) {
|
||||||
|
for (const oldSession of oldSessions) {
|
||||||
|
const userId = oldDbIdMap.users[oldSession.userId] || null // Can be null
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate Device
|
||||||
|
//
|
||||||
|
let deviceId = null
|
||||||
|
if (oldSession.deviceInfo) {
|
||||||
|
const oldDeviceInfo = oldSession.deviceInfo
|
||||||
|
const deviceDeviceId = getDeviceInfoString(oldDeviceInfo, userId)
|
||||||
|
deviceId = oldDbIdMap.devices[deviceDeviceId]
|
||||||
|
if (!deviceId) {
|
||||||
|
let clientName = 'Unknown'
|
||||||
|
let clientVersion = null
|
||||||
|
let deviceName = null
|
||||||
|
let deviceVersion = oldDeviceInfo.browserVersion || null
|
||||||
|
let extraData = {}
|
||||||
|
if (oldDeviceInfo.sdkVersion) {
|
||||||
|
clientName = 'Abs Android'
|
||||||
|
clientVersion = oldDeviceInfo.clientVersion || null
|
||||||
|
deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}`
|
||||||
|
deviceVersion = oldDeviceInfo.sdkVersion
|
||||||
|
} else if (oldDeviceInfo.model) {
|
||||||
|
clientName = 'Abs iOS'
|
||||||
|
clientVersion = oldDeviceInfo.clientVersion || null
|
||||||
|
deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}`
|
||||||
|
} else if (oldDeviceInfo.osName && oldDeviceInfo.browserName) {
|
||||||
|
clientName = 'Abs Web'
|
||||||
|
clientVersion = oldDeviceInfo.serverVersion || null
|
||||||
|
deviceName = `${oldDeviceInfo.osName} ${oldDeviceInfo.osVersion || 'N/A'} ${oldDeviceInfo.browserName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDeviceInfo.manufacturer) {
|
||||||
|
extraData.manufacturer = oldDeviceInfo.manufacturer
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.model) {
|
||||||
|
extraData.model = oldDeviceInfo.model
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.osName) {
|
||||||
|
extraData.osName = oldDeviceInfo.osName
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.osVersion) {
|
||||||
|
extraData.osVersion = oldDeviceInfo.osVersion
|
||||||
|
}
|
||||||
|
if (oldDeviceInfo.browserName) {
|
||||||
|
extraData.browserName = oldDeviceInfo.browserName
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = uuidv4()
|
||||||
|
const Device = {
|
||||||
|
id,
|
||||||
|
deviceId: deviceDeviceId,
|
||||||
|
clientName,
|
||||||
|
clientVersion,
|
||||||
|
ipAddress: oldDeviceInfo.ipAddress,
|
||||||
|
deviceName, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||||
|
deviceVersion,
|
||||||
|
userId,
|
||||||
|
extraData
|
||||||
|
}
|
||||||
|
newRecords.device.push(Device)
|
||||||
|
oldDbIdMap.devices[deviceDeviceId] = Device.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate PlaybackSession
|
||||||
|
//
|
||||||
|
let mediaItemId = null
|
||||||
|
let mediaItemType = 'book'
|
||||||
|
if (oldSession.mediaType === 'podcast') {
|
||||||
|
mediaItemId = oldDbIdMap.podcastEpisodes[oldSession.episodeId] || null
|
||||||
|
mediaItemType = 'podcastEpisode'
|
||||||
|
} else {
|
||||||
|
mediaItemId = oldDbIdMap.books[oldSession.libraryItemId] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaybackSession = {
|
||||||
|
id: uuidv4(),
|
||||||
|
mediaItemId, // Can be null
|
||||||
|
mediaItemType,
|
||||||
|
libraryId: oldDbIdMap.libraries[oldSession.libraryId] || null,
|
||||||
|
displayTitle: oldSession.displayTitle,
|
||||||
|
displayAuthor: oldSession.displayAuthor,
|
||||||
|
duration: oldSession.duration,
|
||||||
|
playMethod: oldSession.playMethod,
|
||||||
|
mediaPlayer: oldSession.mediaPlayer,
|
||||||
|
startTime: oldSession.startTime,
|
||||||
|
currentTime: oldSession.currentTime,
|
||||||
|
serverVersion: oldSession.deviceInfo?.serverVersion || null,
|
||||||
|
createdAt: oldSession.startedAt,
|
||||||
|
updatedAt: oldSession.updatedAt,
|
||||||
|
userId, // Can be null
|
||||||
|
deviceId,
|
||||||
|
timeListening: oldSession.timeListening,
|
||||||
|
coverPath: oldSession.coverPath,
|
||||||
|
mediaMetadata: oldSession.mediaMetadata,
|
||||||
|
date: oldSession.date,
|
||||||
|
dayOfWeek: oldSession.dayOfWeek,
|
||||||
|
extraData: {
|
||||||
|
libraryItemId: oldDbIdMap.libraryItems[oldSession.libraryItemId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRecords.playbackSession.push(PlaybackSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateCollections(oldCollections) {
|
||||||
|
for (const oldCollection of oldCollections) {
|
||||||
|
const libraryId = oldDbIdMap.libraries[oldCollection.libraryId]
|
||||||
|
if (!libraryId) {
|
||||||
|
Logger.warn(`[dbMigration] migrateCollections: Library not found for collection "${oldCollection.name}" (id:${oldCollection.libraryId})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid)
|
||||||
|
if (!BookIds.length) {
|
||||||
|
Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const Collection = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: oldCollection.name,
|
||||||
|
description: oldCollection.description,
|
||||||
|
createdAt: oldCollection.createdAt,
|
||||||
|
updatedAt: oldCollection.lastUpdate,
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
oldDbIdMap.collections[oldCollection.id] = Collection.id
|
||||||
|
newRecords.collection.push(Collection)
|
||||||
|
|
||||||
|
let order = 1
|
||||||
|
BookIds.forEach((bookId) => {
|
||||||
|
const CollectionBook = {
|
||||||
|
id: uuidv4(),
|
||||||
|
createdAt: Collection.createdAt,
|
||||||
|
bookId,
|
||||||
|
collectionId: Collection.id,
|
||||||
|
order: order++
|
||||||
|
}
|
||||||
|
newRecords.collectionBook.push(CollectionBook)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migratePlaylists(oldPlaylists) {
|
||||||
|
for (const oldPlaylist of oldPlaylists) {
|
||||||
|
const libraryId = oldDbIdMap.libraries[oldPlaylist.libraryId]
|
||||||
|
if (!libraryId) {
|
||||||
|
Logger.warn(`[dbMigration] migratePlaylists: Library not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.libraryId})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = oldDbIdMap.users[oldPlaylist.userId]
|
||||||
|
if (!userId) {
|
||||||
|
Logger.warn(`[dbMigration] migratePlaylists: User not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.userId})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaItemType = 'book'
|
||||||
|
let MediaItemIds = []
|
||||||
|
oldPlaylist.items.forEach((itemObj) => {
|
||||||
|
if (itemObj.episodeId) {
|
||||||
|
mediaItemType = 'podcastEpisode'
|
||||||
|
if (oldDbIdMap.podcastEpisodes[itemObj.episodeId]) {
|
||||||
|
MediaItemIds.push(oldDbIdMap.podcastEpisodes[itemObj.episodeId])
|
||||||
|
}
|
||||||
|
} else if (oldDbIdMap.books[itemObj.libraryItemId]) {
|
||||||
|
MediaItemIds.push(oldDbIdMap.books[itemObj.libraryItemId])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!MediaItemIds.length) {
|
||||||
|
Logger.warn(`[dbMigration] migratePlaylists: Playlist "${oldPlaylist.name}" has no items`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const Playlist = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: oldPlaylist.name,
|
||||||
|
description: oldPlaylist.description,
|
||||||
|
createdAt: oldPlaylist.createdAt,
|
||||||
|
updatedAt: oldPlaylist.lastUpdate,
|
||||||
|
userId,
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
newRecords.playlist.push(Playlist)
|
||||||
|
|
||||||
|
let order = 1
|
||||||
|
MediaItemIds.forEach((mediaItemId) => {
|
||||||
|
const PlaylistMediaItem = {
|
||||||
|
id: uuidv4(),
|
||||||
|
mediaItemId,
|
||||||
|
mediaItemType,
|
||||||
|
createdAt: Playlist.createdAt,
|
||||||
|
playlistId: Playlist.id,
|
||||||
|
order: order++
|
||||||
|
}
|
||||||
|
newRecords.playlistMediaItem.push(PlaylistMediaItem)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateFeeds(oldFeeds) {
|
||||||
|
for (const oldFeed of oldFeeds) {
|
||||||
|
if (!oldFeed.episodes?.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let entityId = null
|
||||||
|
|
||||||
|
if (oldFeed.entityType === 'collection') {
|
||||||
|
entityId = oldDbIdMap.collections[oldFeed.entityId]
|
||||||
|
} else if (oldFeed.entityType === 'libraryItem') {
|
||||||
|
entityId = oldDbIdMap.libraryItems[oldFeed.entityId]
|
||||||
|
} else if (oldFeed.entityType === 'series') {
|
||||||
|
entityId = oldDbIdMap.series[oldFeed.entityId]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
Logger.warn(`[dbMigration] migrateFeeds: Entity not found for feed "${oldFeed.entityType}" (id:${oldFeed.entityId})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = oldDbIdMap.users[oldFeed.userId]
|
||||||
|
if (!userId) {
|
||||||
|
Logger.warn(`[dbMigration] migrateFeeds: User not found for feed (id:${oldFeed.userId})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldFeedMeta = oldFeed.meta
|
||||||
|
|
||||||
|
const Feed = {
|
||||||
|
id: uuidv4(),
|
||||||
|
slug: oldFeed.slug,
|
||||||
|
entityType: oldFeed.entityType,
|
||||||
|
entityId,
|
||||||
|
entityUpdatedAt: oldFeed.entityUpdatedAt,
|
||||||
|
serverAddress: oldFeed.serverAddress,
|
||||||
|
feedURL: oldFeed.feedUrl,
|
||||||
|
imageURL: oldFeedMeta.imageUrl,
|
||||||
|
siteURL: oldFeedMeta.link,
|
||||||
|
title: oldFeedMeta.title,
|
||||||
|
description: oldFeedMeta.description,
|
||||||
|
author: oldFeedMeta.author,
|
||||||
|
podcastType: oldFeedMeta.type || null,
|
||||||
|
language: oldFeedMeta.language || null,
|
||||||
|
ownerName: oldFeedMeta.ownerName || null,
|
||||||
|
ownerEmail: oldFeedMeta.ownerEmail || null,
|
||||||
|
explicit: !!oldFeedMeta.explicit,
|
||||||
|
preventIndexing: !!oldFeedMeta.preventIndexing,
|
||||||
|
createdAt: oldFeed.createdAt,
|
||||||
|
updatedAt: oldFeed.updatedAt,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
newRecords.feed.push(Feed)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Migrate FeedEpisodes
|
||||||
|
//
|
||||||
|
for (const oldFeedEpisode of oldFeed.episodes) {
|
||||||
|
const FeedEpisode = {
|
||||||
|
id: uuidv4(),
|
||||||
|
title: oldFeedEpisode.title,
|
||||||
|
author: oldFeedEpisode.author,
|
||||||
|
description: oldFeedEpisode.description,
|
||||||
|
siteURL: oldFeedEpisode.link,
|
||||||
|
enclosureURL: oldFeedEpisode.enclosure?.url || null,
|
||||||
|
enclosureType: oldFeedEpisode.enclosure?.type || null,
|
||||||
|
enclosureSize: oldFeedEpisode.enclosure?.size || null,
|
||||||
|
pubDate: oldFeedEpisode.pubDate,
|
||||||
|
season: oldFeedEpisode.season || null,
|
||||||
|
episode: oldFeedEpisode.episode || null,
|
||||||
|
episodeType: oldFeedEpisode.episodeType || null,
|
||||||
|
duration: oldFeedEpisode.duration,
|
||||||
|
filePath: oldFeedEpisode.fullPath,
|
||||||
|
explicit: !!oldFeedEpisode.explicit,
|
||||||
|
createdAt: oldFeed.createdAt,
|
||||||
|
updatedAt: oldFeed.updatedAt,
|
||||||
|
feedId: Feed.id
|
||||||
|
}
|
||||||
|
newRecords.feedEpisode.push(FeedEpisode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSettings(oldSettings) {
|
||||||
|
const serverSettings = oldSettings.find(s => s.id === 'server-settings')
|
||||||
|
const notificationSettings = oldSettings.find(s => s.id === 'notification-settings')
|
||||||
|
const emailSettings = oldSettings.find(s => s.id === 'email-settings')
|
||||||
|
|
||||||
|
if (serverSettings) {
|
||||||
|
newRecords.setting.push({
|
||||||
|
key: 'server-settings',
|
||||||
|
value: serverSettings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationSettings) {
|
||||||
|
newRecords.setting.push({
|
||||||
|
key: 'notification-settings',
|
||||||
|
value: notificationSettings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailSettings) {
|
||||||
|
newRecords.setting.push({
|
||||||
|
key: 'email-settings',
|
||||||
|
value: emailSettings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.migrate = async (DatabaseModels) => {
|
||||||
|
Logger.info(`[dbMigration] Starting migration`)
|
||||||
|
|
||||||
|
const data = await oldDbFiles.init()
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
migrateSettings(data.settings)
|
||||||
|
migrateAuthors(data.authors)
|
||||||
|
migrateSeries(data.series)
|
||||||
|
migrateLibraries(data.libraries)
|
||||||
|
migrateLibraryItems(data.libraryItems)
|
||||||
|
migrateUsers(data.users)
|
||||||
|
migrateSessions(data.sessions)
|
||||||
|
migrateCollections(data.collections)
|
||||||
|
migratePlaylists(data.playlists)
|
||||||
|
migrateFeeds(data.feeds)
|
||||||
|
|
||||||
|
let totalRecords = 0
|
||||||
|
for (const model in newRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newRecords[model].length} ${model} rows`)
|
||||||
|
if (newRecords[model].length) {
|
||||||
|
await DatabaseModels[model].bulkCreate(newRecords[model])
|
||||||
|
totalRecords += newRecords[model].length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Date.now() - start
|
||||||
|
|
||||||
|
// Purge author images and cover images from cache
|
||||||
|
try {
|
||||||
|
const CachePath = Path.join(global.MetadataPath, 'cache')
|
||||||
|
await fs.emptyDir(Path.join(CachePath, 'covers'))
|
||||||
|
await fs.emptyDir(Path.join(CachePath, 'images'))
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[dbMigration] Failed to purge author/cover image cache`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put all old db folders into a zipfile oldDb.zip
|
||||||
|
await oldDbFiles.zipWrapOldDb()
|
||||||
|
|
||||||
|
Logger.info(`[dbMigration] Migration complete. ${totalRecords} rows. Elapsed ${(elapsed / 1000).toFixed(2)}s`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} true if old database exists
|
||||||
|
*/
|
||||||
|
module.exports.checkShouldMigrate = async (force = false) => {
|
||||||
|
if (await oldDbFiles.checkHasOldDb()) return true
|
||||||
|
if (!force) return false
|
||||||
|
return oldDbFiles.checkHasOldDbZip()
|
||||||
|
}
|
189
server/utils/migrations/oldDbFiles.js
Normal file
189
server/utils/migrations/oldDbFiles.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
const { once } = require('events')
|
||||||
|
const { createInterface } = require('readline')
|
||||||
|
const Path = require('path')
|
||||||
|
const Logger = require('../../Logger')
|
||||||
|
const fs = require('../../libs/fsExtra')
|
||||||
|
const archiver = require('../../libs/archiver')
|
||||||
|
const StreamZip = require('../../libs/nodeStreamZip')
|
||||||
|
|
||||||
|
async function processDbFile(filepath) {
|
||||||
|
if (!fs.pathExistsSync(filepath)) {
|
||||||
|
Logger.error(`[oldDbFiles] Db file does not exist at "${filepath}"`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileStream = fs.createReadStream(filepath)
|
||||||
|
|
||||||
|
const rl = createInterface({
|
||||||
|
input: fileStream,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
})
|
||||||
|
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
if (line && line.trim()) {
|
||||||
|
try {
|
||||||
|
const entity = JSON.parse(line)
|
||||||
|
if (entity && Object.keys(entity).length) entities.push(entity)
|
||||||
|
} catch (jsonParseError) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to parse line "${line}" in db file "${filepath}"`, jsonParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await once(rl, 'close')
|
||||||
|
|
||||||
|
console.log(`[oldDbFiles] Db file "${filepath}" processed`)
|
||||||
|
|
||||||
|
return entities
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to read db file "${filepath}"`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDbData(dbpath) {
|
||||||
|
try {
|
||||||
|
Logger.info(`[oldDbFiles] Loading db data at "${dbpath}"`)
|
||||||
|
const files = await fs.readdir(dbpath)
|
||||||
|
|
||||||
|
const entities = []
|
||||||
|
for (const filename of files) {
|
||||||
|
if (Path.extname(filename).toLowerCase() !== '.json') {
|
||||||
|
Logger.warn(`[oldDbFiles] Ignoring filename "${filename}" in db folder "${dbpath}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = Path.join(dbpath, filename)
|
||||||
|
Logger.info(`[oldDbFiles] Loading db data file "${filepath}"`)
|
||||||
|
const someEntities = await processDbFile(filepath)
|
||||||
|
Logger.info(`[oldDbFiles] Processed db data file with ${someEntities.length} entities`)
|
||||||
|
entities.push(...someEntities)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[oldDbFiles] Finished loading db data with ${entities.length} entities`)
|
||||||
|
return entities
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to load db data "${dbpath}"`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.init = async () => {
|
||||||
|
const dbs = {
|
||||||
|
libraryItems: Path.join(global.ConfigPath, 'libraryItems', 'data'),
|
||||||
|
users: Path.join(global.ConfigPath, 'users', 'data'),
|
||||||
|
sessions: Path.join(global.ConfigPath, 'sessions', 'data'),
|
||||||
|
libraries: Path.join(global.ConfigPath, 'libraries', 'data'),
|
||||||
|
settings: Path.join(global.ConfigPath, 'settings', 'data'),
|
||||||
|
collections: Path.join(global.ConfigPath, 'collections', 'data'),
|
||||||
|
playlists: Path.join(global.ConfigPath, 'playlists', 'data'),
|
||||||
|
authors: Path.join(global.ConfigPath, 'authors', 'data'),
|
||||||
|
series: Path.join(global.ConfigPath, 'series', 'data'),
|
||||||
|
feeds: Path.join(global.ConfigPath, 'feeds', 'data')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {}
|
||||||
|
for (const key in dbs) {
|
||||||
|
data[key] = await loadDbData(dbs[key])
|
||||||
|
Logger.info(`[oldDbFiles] ${data[key].length} ${key} loaded`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.zipWrapOldDb = async () => {
|
||||||
|
const dbs = {
|
||||||
|
libraryItems: Path.join(global.ConfigPath, 'libraryItems'),
|
||||||
|
users: Path.join(global.ConfigPath, 'users'),
|
||||||
|
sessions: Path.join(global.ConfigPath, 'sessions'),
|
||||||
|
libraries: Path.join(global.ConfigPath, 'libraries'),
|
||||||
|
settings: Path.join(global.ConfigPath, 'settings'),
|
||||||
|
collections: Path.join(global.ConfigPath, 'collections'),
|
||||||
|
playlists: Path.join(global.ConfigPath, 'playlists'),
|
||||||
|
authors: Path.join(global.ConfigPath, 'authors'),
|
||||||
|
series: Path.join(global.ConfigPath, 'series'),
|
||||||
|
feeds: Path.join(global.ConfigPath, 'feeds')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||||
|
const output = fs.createWriteStream(oldDbPath)
|
||||||
|
const archive = archiver('zip', {
|
||||||
|
zlib: { level: 9 } // Sets the compression level.
|
||||||
|
})
|
||||||
|
|
||||||
|
// listen for all archive data to be written
|
||||||
|
// 'close' event is fired only when a file descriptor is involved
|
||||||
|
output.on('close', async () => {
|
||||||
|
Logger.info(`[oldDbFiles] Old db files have been zipped in ${oldDbPath}. ${archive.pointer()} total bytes`)
|
||||||
|
|
||||||
|
// Remove old db folders have successful zip
|
||||||
|
for (const db in dbs) {
|
||||||
|
await fs.remove(dbs[db])
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// This event is fired when the data source is drained no matter what was the data source.
|
||||||
|
// It is not part of this library but rather from the NodeJS Stream API.
|
||||||
|
// @see: https://nodejs.org/api/stream.html#stream_event_end
|
||||||
|
output.on('end', () => {
|
||||||
|
Logger.debug('[oldDbFiles] Data has been drained')
|
||||||
|
})
|
||||||
|
|
||||||
|
// good practice to catch this error explicitly
|
||||||
|
archive.on('error', (err) => {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to zip old db folders`, err)
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// pipe archive data to the file
|
||||||
|
archive.pipe(output)
|
||||||
|
|
||||||
|
for (const db in dbs) {
|
||||||
|
archive.directory(dbs[db], db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize the archive (ie we are done appending files but streams have to finish yet)
|
||||||
|
// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
|
||||||
|
archive.finalize()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.checkHasOldDb = async () => {
|
||||||
|
const dbs = {
|
||||||
|
libraryItems: Path.join(global.ConfigPath, 'libraryItems'),
|
||||||
|
users: Path.join(global.ConfigPath, 'users'),
|
||||||
|
sessions: Path.join(global.ConfigPath, 'sessions'),
|
||||||
|
libraries: Path.join(global.ConfigPath, 'libraries'),
|
||||||
|
settings: Path.join(global.ConfigPath, 'settings'),
|
||||||
|
collections: Path.join(global.ConfigPath, 'collections'),
|
||||||
|
playlists: Path.join(global.ConfigPath, 'playlists'),
|
||||||
|
authors: Path.join(global.ConfigPath, 'authors'),
|
||||||
|
series: Path.join(global.ConfigPath, 'series'),
|
||||||
|
feeds: Path.join(global.ConfigPath, 'feeds')
|
||||||
|
}
|
||||||
|
for (const db in dbs) {
|
||||||
|
if (await fs.pathExists(dbs[db])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.checkHasOldDbZip = async () => {
|
||||||
|
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||||
|
if (!await fs.pathExists(oldDbPath)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract oldDb.zip
|
||||||
|
const zip = new StreamZip.async({ file: oldDbPath })
|
||||||
|
await zip.extract(null, global.ConfigPath)
|
||||||
|
|
||||||
|
return this.checkHasOldDb()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user