mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-23 21:51:56 +01:00
Init sqlite take 2
This commit is contained in:
parent
d86a3b3dc2
commit
cf7fd315b6
@ -154,7 +154,7 @@ export default {
|
||||
}
|
||||
this.deletingDeviceName = device.name
|
||||
this.$axios
|
||||
.$patch(`/emails/ereader-devices`, payload)
|
||||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||
this.$toast.success('Device deleted')
|
||||
|
@ -191,6 +191,7 @@ export default class PlayerHandler {
|
||||
|
||||
const payload = {
|
||||
deviceInfo: {
|
||||
clientName: 'Abs Web',
|
||||
deviceId: this.getDeviceId()
|
||||
},
|
||||
supportedMimeTypes: this.player.playableMimeTypes,
|
||||
|
2376
package-lock.json
generated
2376
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,9 @@
|
||||
"htmlparser2": "^8.0.1",
|
||||
"node-tone": "^1.0.1",
|
||||
"nodemailer": "^6.9.2",
|
||||
"sequelize": "^6.32.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"sqlite3": "^5.1.6",
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2,21 +2,10 @@ const bcrypt = require('./libs/bcryptjs')
|
||||
const jwt = require('./libs/jsonwebtoken')
|
||||
const requestIp = require('./libs/requestIp')
|
||||
const Logger = require('./Logger')
|
||||
const Database = require('./Database')
|
||||
|
||||
class Auth {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
|
||||
this.user = null
|
||||
}
|
||||
|
||||
get username() {
|
||||
return this.user ? this.user.username : 'nobody'
|
||||
}
|
||||
|
||||
get users() {
|
||||
return this.db.users
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
cors(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
@ -35,20 +24,20 @@ class Auth {
|
||||
async initTokenSecret() {
|
||||
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
||||
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
||||
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||
Database.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||
} else {
|
||||
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
||||
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||
Database.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||
}
|
||||
await this.db.updateServerSettings()
|
||||
await Database.updateServerSettings()
|
||||
|
||||
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||
if (this.db.users.length) {
|
||||
for (const user of this.db.users) {
|
||||
if (Database.users.length) {
|
||||
for (const user of Database.users) {
|
||||
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
||||
}
|
||||
await this.db.updateEntities('user', this.db.users)
|
||||
await Database.updateBulkUsers(Database.users)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +57,7 @@ class Auth {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
var user = await this.verifyToken(token)
|
||||
const user = await this.verifyToken(token)
|
||||
if (!user) {
|
||||
Logger.error('Verify Token User Not Found', token)
|
||||
return res.sendStatus(404)
|
||||
@ -95,7 +84,7 @@ class Auth {
|
||||
}
|
||||
|
||||
generateAccessToken(payload) {
|
||||
return jwt.sign(payload, global.ServerSettings.tokenSecret);
|
||||
return jwt.sign(payload, Database.serverSettings.tokenSecret)
|
||||
}
|
||||
|
||||
authenticateUser(token) {
|
||||
@ -104,12 +93,12 @@ class Auth {
|
||||
|
||||
verifyToken(token) {
|
||||
return new Promise((resolve) => {
|
||||
jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => {
|
||||
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
|
||||
if (!payload || err) {
|
||||
Logger.error('JWT Verify Token Failed', err)
|
||||
return resolve(null)
|
||||
}
|
||||
const user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||
const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username)
|
||||
resolve(user || null)
|
||||
})
|
||||
})
|
||||
@ -118,9 +107,9 @@ class Auth {
|
||||
getUserLoginResponsePayload(user) {
|
||||
return {
|
||||
user: user.toJSONForBrowser(),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||
ereaderDevices: this.db.emailSettings.getEReaderDevices(user),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||
Source: global.Source
|
||||
}
|
||||
}
|
||||
@ -130,7 +119,7 @@ class Auth {
|
||||
const username = (req.body.username || '').toLowerCase()
|
||||
const password = req.body.password || ''
|
||||
|
||||
const user = this.users.find(u => u.username.toLowerCase() === username)
|
||||
const user = Database.users.find(u => u.username.toLowerCase() === username)
|
||||
|
||||
if (!user?.isActive) {
|
||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||
@ -142,7 +131,7 @@ class Auth {
|
||||
}
|
||||
|
||||
// Check passwordless root user
|
||||
if (user.id === 'root' && (!user.pash || user.pash === '')) {
|
||||
if (user.type === 'root' && (!user.pash || user.pash === '')) {
|
||||
if (password) {
|
||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||
} else {
|
||||
@ -166,15 +155,6 @@ class Auth {
|
||||
}
|
||||
}
|
||||
|
||||
// Not in use now
|
||||
lockUser(user) {
|
||||
user.isLocked = true
|
||||
return this.db.updateEntity('user', user).catch((error) => {
|
||||
Logger.error('[Auth] Failed to lock user', user.username, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
comparePassword(password, user) {
|
||||
if (user.type === 'root' && !password && !user.pash) return true
|
||||
if (!password || !user.pash) return false
|
||||
@ -184,7 +164,7 @@ class Auth {
|
||||
async userChangePassword(req, res) {
|
||||
var { password, newPassword } = req.body
|
||||
newPassword = newPassword || ''
|
||||
var matchingUser = this.users.find(u => u.id === req.user.id)
|
||||
const matchingUser = Database.users.find(u => u.id === req.user.id)
|
||||
|
||||
// Only root can have an empty password
|
||||
if (matchingUser.type !== 'root' && !newPassword) {
|
||||
@ -193,14 +173,14 @@ class Auth {
|
||||
})
|
||||
}
|
||||
|
||||
var compare = await this.comparePassword(password, matchingUser)
|
||||
const compare = await this.comparePassword(password, matchingUser)
|
||||
if (!compare) {
|
||||
return res.json({
|
||||
error: 'Invalid password'
|
||||
})
|
||||
}
|
||||
|
||||
var pw = ''
|
||||
let pw = ''
|
||||
if (newPassword) {
|
||||
pw = await this.hashPass(newPassword)
|
||||
if (!pw) {
|
||||
@ -211,7 +191,8 @@ class Auth {
|
||||
}
|
||||
|
||||
matchingUser.pash = pw
|
||||
var success = await this.db.updateEntity('user', matchingUser)
|
||||
|
||||
const success = await Database.updateUser(matchingUser)
|
||||
if (success) {
|
||||
res.json({
|
||||
success: true
|
||||
|
456
server/Database.js
Normal file
456
server/Database.js
Normal file
@ -0,0 +1,456 @@
|
||||
const Path = require('path')
|
||||
const { Sequelize } = require('sequelize')
|
||||
|
||||
const packageJson = require('../package.json')
|
||||
const fs = require('./libs/fsExtra')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
const dbMigration = require('./utils/migrations/dbMigration')
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.sequelize = null
|
||||
this.dbPath = null
|
||||
this.isNew = false // New database.sqlite created
|
||||
|
||||
// Temporarily using format of old DB
|
||||
// below data should be loaded from the DB as needed
|
||||
this.libraryItems = []
|
||||
this.users = []
|
||||
this.libraries = []
|
||||
this.settings = []
|
||||
this.collections = []
|
||||
this.playlists = []
|
||||
this.authors = []
|
||||
this.series = []
|
||||
this.feeds = []
|
||||
|
||||
this.serverSettings = null
|
||||
this.notificationSettings = null
|
||||
this.emailSettings = null
|
||||
}
|
||||
|
||||
get models() {
|
||||
return this.sequelize?.models || {}
|
||||
}
|
||||
|
||||
get hasRootUser() {
|
||||
return this.users.some(u => u.type === 'root')
|
||||
}
|
||||
|
||||
async checkHasDb() {
|
||||
if (!await fs.pathExists(this.dbPath)) {
|
||||
Logger.info(`[Database] database.sqlite not found at ${this.dbPath}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async init(force = false) {
|
||||
this.dbPath = Path.join(global.ConfigPath, 'database.sqlite')
|
||||
|
||||
// First check if this is a new database
|
||||
this.isNew = !(await this.checkHasDb()) || force
|
||||
|
||||
if (!await this.connect()) {
|
||||
throw new Error('Database connection failed')
|
||||
}
|
||||
|
||||
await this.buildModels(force)
|
||||
Logger.info(`[Database] Db initialized`, Object.keys(this.sequelize.models))
|
||||
|
||||
await this.loadData(force)
|
||||
}
|
||||
|
||||
async connect() {
|
||||
Logger.info(`[Database] Initializing db at "${this.dbPath}"`)
|
||||
this.sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: this.dbPath,
|
||||
logging: false
|
||||
})
|
||||
|
||||
// Helper function
|
||||
this.sequelize.uppercaseFirst = str => str ? `${str[0].toUpperCase()}${str.substr(1)}` : ''
|
||||
|
||||
try {
|
||||
await this.sequelize.authenticate()
|
||||
Logger.info(`[Database] Db connection was successful`)
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error(`[Database] Failed to connect to db`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
buildModels(force = false) {
|
||||
require('./models/User')(this.sequelize)
|
||||
require('./models/Library')(this.sequelize)
|
||||
require('./models/LibraryFolder')(this.sequelize)
|
||||
require('./models/Book')(this.sequelize)
|
||||
require('./models/Podcast')(this.sequelize)
|
||||
require('./models/PodcastEpisode')(this.sequelize)
|
||||
require('./models/LibraryItem')(this.sequelize)
|
||||
require('./models/MediaProgress')(this.sequelize)
|
||||
require('./models/Series')(this.sequelize)
|
||||
require('./models/BookSeries')(this.sequelize)
|
||||
require('./models/Author')(this.sequelize)
|
||||
require('./models/BookAuthor')(this.sequelize)
|
||||
require('./models/Collection')(this.sequelize)
|
||||
require('./models/CollectionBook')(this.sequelize)
|
||||
require('./models/Playlist')(this.sequelize)
|
||||
require('./models/PlaylistMediaItem')(this.sequelize)
|
||||
require('./models/Device')(this.sequelize)
|
||||
require('./models/PlaybackSession')(this.sequelize)
|
||||
require('./models/Feed')(this.sequelize)
|
||||
require('./models/FeedEpisode')(this.sequelize)
|
||||
require('./models/Setting')(this.sequelize)
|
||||
|
||||
return this.sequelize.sync({ force })
|
||||
}
|
||||
|
||||
async loadData(force = false) {
|
||||
if (this.isNew && await dbMigration.checkShouldMigrate(force)) {
|
||||
Logger.info(`[Database] New database was created and old database was detected - migrating old to new`)
|
||||
await dbMigration.migrate(this.models)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
this.libraryItems = await this.models.libraryItem.getAllOldLibraryItems()
|
||||
this.users = await this.models.user.getOldUsers()
|
||||
this.libraries = await this.models.library.getAllOldLibraries()
|
||||
this.collections = await this.models.collection.getOldCollections()
|
||||
this.playlists = await this.models.playlist.getOldPlaylists()
|
||||
this.authors = await this.models.author.getOldAuthors()
|
||||
this.series = await this.models.series.getAllOldSeries()
|
||||
this.feeds = await this.models.feed.getOldFeeds()
|
||||
|
||||
const settingsData = await this.models.setting.getOldSettings()
|
||||
this.settings = settingsData.settings
|
||||
this.emailSettings = settingsData.emailSettings
|
||||
this.serverSettings = settingsData.serverSettings
|
||||
this.notificationSettings = settingsData.notificationSettings
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
|
||||
Logger.info(`[Database] Db data loaded in ${Date.now() - startTime}ms`)
|
||||
|
||||
if (packageJson.version !== this.serverSettings.version) {
|
||||
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
||||
this.serverSettings.version = packageJson.version
|
||||
await this.updateServerSettings()
|
||||
}
|
||||
}
|
||||
|
||||
async createRootUser(username, pash, token) {
|
||||
const newUser = await this.models.user.createRootUser(username, pash, token)
|
||||
if (newUser) {
|
||||
this.users.push(newUser)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
return this.updateSetting(this.serverSettings)
|
||||
}
|
||||
|
||||
updateSetting(settings) {
|
||||
return this.models.setting.updateSettingObj(settings.toJSON())
|
||||
}
|
||||
|
||||
async createUser(oldUser) {
|
||||
await this.models.user.createFromOld(oldUser)
|
||||
this.users.push(oldUser)
|
||||
return true
|
||||
}
|
||||
|
||||
updateUser(oldUser) {
|
||||
return this.models.user.updateFromOld(oldUser)
|
||||
}
|
||||
|
||||
updateBulkUsers(oldUsers) {
|
||||
return Promise.all(oldUsers.map(u => this.updateUser(u)))
|
||||
}
|
||||
|
||||
async removeUser(userId) {
|
||||
await this.models.user.removeById(userId)
|
||||
this.users = this.users.filter(u => u.id !== userId)
|
||||
}
|
||||
|
||||
upsertMediaProgress(oldMediaProgress) {
|
||||
return this.models.mediaProgress.upsertFromOld(oldMediaProgress)
|
||||
}
|
||||
|
||||
removeMediaProgress(mediaProgressId) {
|
||||
return this.models.mediaProgress.removeById(mediaProgressId)
|
||||
}
|
||||
|
||||
updateBulkBooks(oldBooks) {
|
||||
return Promise.all(oldBooks.map(oldBook => this.models.book.saveFromOld(oldBook)))
|
||||
}
|
||||
|
||||
async createLibrary(oldLibrary) {
|
||||
await this.models.library.createFromOld(oldLibrary)
|
||||
this.libraries.push(oldLibrary)
|
||||
}
|
||||
|
||||
updateLibrary(oldLibrary) {
|
||||
return this.models.library.updateFromOld(oldLibrary)
|
||||
}
|
||||
|
||||
async removeLibrary(libraryId) {
|
||||
await this.models.library.removeById(libraryId)
|
||||
this.libraries = this.libraries.filter(lib => lib.id !== libraryId)
|
||||
}
|
||||
|
||||
async createCollection(oldCollection) {
|
||||
const newCollection = await this.models.collection.createFromOld(oldCollection)
|
||||
// Create CollectionBooks
|
||||
if (newCollection) {
|
||||
const collectionBooks = []
|
||||
oldCollection.books.forEach((libraryItemId) => {
|
||||
const libraryItem = this.libraryItems.filter(li => li.id === libraryItemId)
|
||||
if (libraryItem) {
|
||||
collectionBooks.push({
|
||||
collectionId: newCollection.id,
|
||||
bookId: libraryItem.media.id
|
||||
})
|
||||
}
|
||||
})
|
||||
if (collectionBooks.length) {
|
||||
await this.createBulkCollectionBooks(collectionBooks)
|
||||
}
|
||||
}
|
||||
this.collections.push(oldCollection)
|
||||
}
|
||||
|
||||
updateCollection(oldCollection) {
|
||||
const collectionBooks = []
|
||||
let order = 1
|
||||
oldCollection.books.forEach((libraryItemId) => {
|
||||
const libraryItem = this.getLibraryItem(libraryItemId)
|
||||
if (!libraryItem) return
|
||||
collectionBooks.push({
|
||||
collectionId: oldCollection.id,
|
||||
bookId: libraryItem.media.id,
|
||||
order: order++
|
||||
})
|
||||
})
|
||||
return this.models.collection.fullUpdateFromOld(oldCollection, collectionBooks)
|
||||
}
|
||||
|
||||
async removeCollection(collectionId) {
|
||||
await this.models.collection.removeById(collectionId)
|
||||
this.collections = this.collections.filter(c => c.id !== collectionId)
|
||||
}
|
||||
|
||||
createCollectionBook(collectionBook) {
|
||||
return this.models.collectionBook.create(collectionBook)
|
||||
}
|
||||
|
||||
createBulkCollectionBooks(collectionBooks) {
|
||||
return this.models.collectionBook.bulkCreate(collectionBooks)
|
||||
}
|
||||
|
||||
removeCollectionBook(collectionId, bookId) {
|
||||
return this.models.collectionBook.removeByIds(collectionId, bookId)
|
||||
}
|
||||
|
||||
async createPlaylist(oldPlaylist) {
|
||||
const newPlaylist = await this.models.playlist.createFromOld(oldPlaylist)
|
||||
if (newPlaylist) {
|
||||
const playlistMediaItems = []
|
||||
let order = 1
|
||||
for (const mediaItemObj of oldPlaylist.items) {
|
||||
const libraryItem = this.libraryItems.find(li => li.id === mediaItemObj.libraryItemId)
|
||||
if (!libraryItem) continue
|
||||
|
||||
let mediaItemId = libraryItem.media.id // bookId
|
||||
let mediaItemType = 'book'
|
||||
if (mediaItemObj.episodeId) {
|
||||
mediaItemType = 'podcastEpisode'
|
||||
mediaItemId = mediaItemObj.episodeId
|
||||
}
|
||||
playlistMediaItems.push({
|
||||
playlistId: newPlaylist.id,
|
||||
mediaItemId,
|
||||
mediaItemType,
|
||||
order: order++
|
||||
})
|
||||
}
|
||||
if (playlistMediaItems.length) {
|
||||
await this.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||
}
|
||||
}
|
||||
this.playlists.push(oldPlaylist)
|
||||
}
|
||||
|
||||
updatePlaylist(oldPlaylist) {
|
||||
const playlistMediaItems = []
|
||||
let order = 1
|
||||
oldPlaylist.items.forEach((item) => {
|
||||
const libraryItem = this.getLibraryItem(item.libraryItemId)
|
||||
if (!libraryItem) return
|
||||
playlistMediaItems.push({
|
||||
playlistId: oldPlaylist.id,
|
||||
mediaItemId: item.episodeId || libraryItem.media.id,
|
||||
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: order++
|
||||
})
|
||||
})
|
||||
return this.models.playlist.fullUpdateFromOld(oldPlaylist, playlistMediaItems)
|
||||
}
|
||||
|
||||
async removePlaylist(playlistId) {
|
||||
await this.models.playlist.removeById(playlistId)
|
||||
this.playlists = this.playlists.filter(p => p.id !== playlistId)
|
||||
}
|
||||
|
||||
createPlaylistMediaItem(playlistMediaItem) {
|
||||
return this.models.playlistMediaItem.create(playlistMediaItem)
|
||||
}
|
||||
|
||||
createBulkPlaylistMediaItems(playlistMediaItems) {
|
||||
return this.models.playlistMediaItem.bulkCreate(playlistMediaItems)
|
||||
}
|
||||
|
||||
removePlaylistMediaItem(playlistId, mediaItemId) {
|
||||
return this.models.playlistMediaItem.removeByIds(playlistId, mediaItemId)
|
||||
}
|
||||
|
||||
getLibraryItem(libraryItemId) {
|
||||
return this.libraryItems.find(li => li.id === libraryItemId)
|
||||
}
|
||||
|
||||
async createLibraryItem(oldLibraryItem) {
|
||||
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||
this.libraryItems.push(oldLibraryItem)
|
||||
}
|
||||
|
||||
updateLibraryItem(oldLibraryItem) {
|
||||
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||
}
|
||||
|
||||
async updateBulkLibraryItems(oldLibraryItems) {
|
||||
let updatesMade = 0
|
||||
for (const oldLibraryItem of oldLibraryItems) {
|
||||
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||
if (hasUpdates) updatesMade++
|
||||
}
|
||||
return updatesMade
|
||||
}
|
||||
|
||||
async createBulkLibraryItems(oldLibraryItems) {
|
||||
for (const oldLibraryItem of oldLibraryItems) {
|
||||
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||
this.libraryItems.push(oldLibraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
async removeLibraryItem(libraryItemId) {
|
||||
await this.models.libraryItem.removeById(libraryItemId)
|
||||
this.libraryItems = this.libraryItems.filter(li => li.id !== libraryItemId)
|
||||
}
|
||||
|
||||
createFeed(oldFeed) {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
updateFeed(oldFeed) {
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
async removeFeed(feedId) {
|
||||
await this.models.feed.removeById(feedId)
|
||||
this.feeds = this.feeds.filter(f => f.id !== feedId)
|
||||
}
|
||||
|
||||
updateSeries(oldSeries) {
|
||||
return this.models.series.updateFromOld(oldSeries)
|
||||
}
|
||||
|
||||
async createSeries(oldSeries) {
|
||||
await this.models.series.createFromOld(oldSeries)
|
||||
this.series.push(oldSeries)
|
||||
}
|
||||
|
||||
async createBulkSeries(oldSeriesObjs) {
|
||||
await this.models.series.createBulkFromOld(oldSeriesObjs)
|
||||
this.series.push(...oldSeriesObjs)
|
||||
}
|
||||
|
||||
async removeSeries(seriesId) {
|
||||
await this.models.series.removeById(seriesId)
|
||||
this.series = this.series.filter(se => se.id !== seriesId)
|
||||
}
|
||||
|
||||
async createAuthor(oldAuthor) {
|
||||
await this.models.createFromOld(oldAuthor)
|
||||
this.authors.push(oldAuthor)
|
||||
}
|
||||
|
||||
async createBulkAuthors(oldAuthors) {
|
||||
await this.models.author.createBulkFromOld(oldAuthors)
|
||||
this.authors.push(...oldAuthors)
|
||||
}
|
||||
|
||||
updateAuthor(oldAuthor) {
|
||||
return this.models.author.updateFromOld(oldAuthor)
|
||||
}
|
||||
|
||||
async removeAuthor(authorId) {
|
||||
await this.models.author.removeById(authorId)
|
||||
this.authors = this.authors.filter(au => au.id !== authorId)
|
||||
}
|
||||
|
||||
async createBulkBookAuthors(bookAuthors) {
|
||||
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
||||
this.authors.push(...bookAuthors)
|
||||
}
|
||||
|
||||
async removeBulkBookAuthors(authorId = null, bookId = null) {
|
||||
if (!authorId && !bookId) return
|
||||
await this.models.bookAuthor.removeByIds(authorId, bookId)
|
||||
this.authors = this.authors.filter(au => {
|
||||
if (authorId && au.authorId !== authorId) return true
|
||||
if (bookId && au.bookId !== bookId) return true
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
getPlaybackSessions(where = null) {
|
||||
return this.models.playbackSession.getOldPlaybackSessions(where)
|
||||
}
|
||||
|
||||
getPlaybackSession(sessionId) {
|
||||
return this.models.playbackSession.getById(sessionId)
|
||||
}
|
||||
|
||||
createPlaybackSession(oldSession) {
|
||||
return this.models.playbackSession.createFromOld(oldSession)
|
||||
}
|
||||
|
||||
updatePlaybackSession(oldSession) {
|
||||
return this.models.playbackSession.updateFromOld(oldSession)
|
||||
}
|
||||
|
||||
removePlaybackSession(sessionId) {
|
||||
return this.models.playbackSession.removeById(sessionId)
|
||||
}
|
||||
|
||||
getDeviceByDeviceId(deviceId) {
|
||||
return this.models.device.getOldDeviceByDeviceId(deviceId)
|
||||
}
|
||||
|
||||
updateDevice(oldDevice) {
|
||||
return this.models.device.updateFromOld(oldDevice)
|
||||
}
|
||||
|
||||
createDevice(oldDevice) {
|
||||
return this.models.device.createFromOld(oldDevice)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Database()
|
49
server/Db.js
49
server/Db.js
@ -243,9 +243,6 @@ class Db {
|
||||
getLibraryItem(id) {
|
||||
return this.libraryItems.find(li => li.id === id)
|
||||
}
|
||||
getLibraryItemsInLibrary(libraryId) {
|
||||
return this.libraryItems.filter(li => li.libraryId === libraryId)
|
||||
}
|
||||
|
||||
async updateLibraryItem(libraryItem) {
|
||||
return this.updateLibraryItems([libraryItem])
|
||||
@ -269,26 +266,6 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
async insertLibraryItem(libraryItem) {
|
||||
return this.insertLibraryItems([libraryItem])
|
||||
}
|
||||
|
||||
async insertLibraryItems(libraryItems) {
|
||||
await Promise.all(libraryItems.map(async (li) => {
|
||||
if (li && li.saveMetadata) return li.saveMetadata()
|
||||
return null
|
||||
}))
|
||||
|
||||
return this.libraryItemsDb.insert(libraryItems).then((results) => {
|
||||
Logger.debug(`[DB] Library Items inserted ${results.inserted}`)
|
||||
this.libraryItems = this.libraryItems.concat(libraryItems)
|
||||
return true
|
||||
}).catch((error) => {
|
||||
Logger.error(`[DB] Library Items insert failed ${error}`)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
removeLibraryItem(id) {
|
||||
return this.libraryItemsDb.delete((record) => record.id === id).then((results) => {
|
||||
Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`)
|
||||
@ -303,14 +280,6 @@ class Db {
|
||||
return this.updateEntity('settings', this.serverSettings)
|
||||
}
|
||||
|
||||
getAllEntities(entityName) {
|
||||
const entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.select(() => true).then((results) => results.data).catch((error) => {
|
||||
Logger.error(`[DB] Failed to get all ${entityName}`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
insertEntities(entityName, entities) {
|
||||
var entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.insert(entities).then((results) => {
|
||||
@ -463,15 +432,6 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
getAllSessions(selectFunc = () => true) {
|
||||
return this.sessionsDb.select(selectFunc).then((results) => {
|
||||
return results.data || []
|
||||
}).catch((error) => {
|
||||
Logger.error('[Db] Failed to select sessions', error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
getPlaybackSession(id) {
|
||||
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
|
||||
if (results.data.length) {
|
||||
@ -484,15 +444,6 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
selectUserSessions(userId) {
|
||||
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
||||
return results.data || []
|
||||
}).catch((error) => {
|
||||
Logger.error(`[Db] Failed to select user sessions "${userId}"`, error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
// Check if server was updated and previous version was earlier than param
|
||||
checkPreviousVersionIsBefore(version) {
|
||||
if (!this.previousVersion) return false
|
||||
|
105
server/Server.js
105
server/Server.js
@ -8,18 +8,18 @@ const rateLimit = require('./libs/expressRateLimit')
|
||||
const { version } = require('../package.json')
|
||||
|
||||
// Utils
|
||||
const dbMigration = require('./utils/dbMigration')
|
||||
const filePerms = require('./utils/filePerms')
|
||||
const fileUtils = require('./utils/fileUtils')
|
||||
const globals = require('./utils/globals')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
const Auth = require('./Auth')
|
||||
const Watcher = require('./Watcher')
|
||||
const Scanner = require('./scanner/Scanner')
|
||||
const Db = require('./Db')
|
||||
const Database = require('./Database')
|
||||
const SocketAuthority = require('./SocketAuthority')
|
||||
|
||||
const routes = require('./routes/index')
|
||||
|
||||
const ApiRouter = require('./routers/ApiRouter')
|
||||
const HlsRouter = require('./routers/HlsRouter')
|
||||
|
||||
@ -29,7 +29,7 @@ const CoverManager = require('./managers/CoverManager')
|
||||
const AbMergeManager = require('./managers/AbMergeManager')
|
||||
const CacheManager = require('./managers/CacheManager')
|
||||
const LogManager = require('./managers/LogManager')
|
||||
const BackupManager = require('./managers/BackupManager')
|
||||
// const BackupManager = require('./managers/BackupManager') // TODO
|
||||
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
||||
const PodcastManager = require('./managers/PodcastManager')
|
||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||
@ -59,30 +59,30 @@ class Server {
|
||||
filePerms.setDefaultDirSync(global.MetadataPath, false)
|
||||
}
|
||||
|
||||
this.db = new Db()
|
||||
// this.db = new Db()
|
||||
this.watcher = new Watcher()
|
||||
this.auth = new Auth(this.db)
|
||||
this.auth = new Auth()
|
||||
|
||||
// Managers
|
||||
this.taskManager = new TaskManager()
|
||||
this.notificationManager = new NotificationManager(this.db)
|
||||
this.emailManager = new EmailManager(this.db)
|
||||
this.backupManager = new BackupManager(this.db)
|
||||
this.logManager = new LogManager(this.db)
|
||||
this.notificationManager = new NotificationManager()
|
||||
this.emailManager = new EmailManager()
|
||||
// this.backupManager = new BackupManager(this.db)
|
||||
this.logManager = new LogManager()
|
||||
this.cacheManager = new CacheManager()
|
||||
this.abMergeManager = new AbMergeManager(this.db, this.taskManager)
|
||||
this.playbackSessionManager = new PlaybackSessionManager(this.db)
|
||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager)
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||
this.rssFeedManager = new RssFeedManager(this.db)
|
||||
this.abMergeManager = new AbMergeManager(this.taskManager)
|
||||
this.playbackSessionManager = new PlaybackSessionManager()
|
||||
this.coverManager = new CoverManager(this.cacheManager)
|
||||
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager)
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager)
|
||||
this.rssFeedManager = new RssFeedManager()
|
||||
|
||||
this.scanner = new Scanner(this.db, this.coverManager, this.taskManager)
|
||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||
this.scanner = new Scanner(this.coverManager, this.taskManager)
|
||||
this.cronManager = new CronManager(this.scanner, this.podcastManager)
|
||||
|
||||
// Routers
|
||||
this.apiRouter = new ApiRouter(this)
|
||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
||||
this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager)
|
||||
|
||||
Logger.logManager = this.logManager
|
||||
|
||||
@ -98,38 +98,28 @@ class Server {
|
||||
Logger.info('[Server] Init v' + version)
|
||||
await this.playbackSessionManager.removeOrphanStreams()
|
||||
|
||||
const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
||||
if (previousVersion) {
|
||||
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
|
||||
}
|
||||
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
|
||||
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
|
||||
await dbMigration.migrate(this.db)
|
||||
} else {
|
||||
await this.db.init()
|
||||
}
|
||||
await Database.init(false)
|
||||
|
||||
// Create token secret if does not exist (Added v2.1.0)
|
||||
if (!this.db.serverSettings.tokenSecret) {
|
||||
if (!Database.serverSettings.tokenSecret) {
|
||||
await this.auth.initTokenSecret()
|
||||
}
|
||||
|
||||
await this.cleanUserData() // Remove invalid user item progress
|
||||
await this.purgeMetadata() // Remove metadata folders without library item
|
||||
await this.playbackSessionManager.removeInvalidSessions()
|
||||
await this.cacheManager.ensureCachePaths()
|
||||
|
||||
await this.backupManager.init()
|
||||
// await this.backupManager.init() // TODO: Implement backups
|
||||
await this.logManager.init()
|
||||
await this.apiRouter.checkRemoveEmptySeries(this.db.series) // Remove empty series
|
||||
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
|
||||
await this.rssFeedManager.init()
|
||||
this.cronManager.init()
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
if (Database.serverSettings.scannerDisableWatcher) {
|
||||
Logger.info(`[Server] Watcher is disabled`)
|
||||
this.watcher.disabled = true
|
||||
} else {
|
||||
this.watcher.initWatcher(this.db.libraries)
|
||||
this.watcher.initWatcher(Database.libraries)
|
||||
this.watcher.on('files', this.filesChanged.bind(this))
|
||||
}
|
||||
}
|
||||
@ -162,6 +152,7 @@ class Server {
|
||||
// Static folder
|
||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||
|
||||
// router.use('/api/v1', routes) // TODO: New routes
|
||||
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
||||
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||
|
||||
@ -203,7 +194,7 @@ class Server {
|
||||
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
||||
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||
router.post('/init', (req, res) => {
|
||||
if (this.db.hasRootUser) {
|
||||
if (Database.hasRootUser) {
|
||||
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
@ -213,8 +204,8 @@ class Server {
|
||||
// status check for client to see if server has been initialized
|
||||
// server has been initialized if a root user exists
|
||||
const payload = {
|
||||
isInit: this.db.hasRootUser,
|
||||
language: this.db.serverSettings.language
|
||||
isInit: Database.hasRootUser,
|
||||
language: Database.serverSettings.language
|
||||
}
|
||||
if (!payload.isInit) {
|
||||
payload.ConfigPath = global.ConfigPath
|
||||
@ -243,7 +234,7 @@ class Server {
|
||||
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
||||
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
||||
await Database.createRootUser(newRoot.username, rootPash, rootToken)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@ -261,7 +252,7 @@ class Server {
|
||||
|
||||
let purged = 0
|
||||
await Promise.all(foldersInItemsMetadata.map(async foldername => {
|
||||
const hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
|
||||
const hasMatchingItem = Database.libraryItems.find(ab => ab.id === foldername)
|
||||
if (!hasMatchingItem) {
|
||||
const folderPath = Path.join(itemsMetadata, foldername)
|
||||
Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
|
||||
@ -281,26 +272,26 @@ class Server {
|
||||
|
||||
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
||||
async cleanUserData() {
|
||||
for (let i = 0; i < this.db.users.length; i++) {
|
||||
const _user = this.db.users[i]
|
||||
let hasUpdated = false
|
||||
for (const _user of Database.users) {
|
||||
if (_user.mediaProgress.length) {
|
||||
const lengthBefore = _user.mediaProgress.length
|
||||
_user.mediaProgress = _user.mediaProgress.filter(mp => {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||
if (!libraryItem) return false
|
||||
if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
|
||||
return true
|
||||
})
|
||||
for (const mediaProgress of _user.mediaProgress) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
|
||||
if (libraryItem && mediaProgress.episodeId) {
|
||||
const episode = libraryItem.media.checkHasEpisode?.(mediaProgress.episodeId)
|
||||
if (episode) continue
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if (lengthBefore > _user.mediaProgress.length) {
|
||||
Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`)
|
||||
hasUpdated = true
|
||||
Logger.debug(`[Server] Removing media progress ${mediaProgress.id} data from user ${_user.username}`)
|
||||
await Database.removeMediaProgress(mediaProgress.id)
|
||||
}
|
||||
}
|
||||
|
||||
let hasUpdated = false
|
||||
if (_user.seriesHideFromContinueListening.length) {
|
||||
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
||||
if (!this.db.series.some(se => se.id === seriesId)) { // Series removed
|
||||
if (!Database.series.some(se => se.id === seriesId)) { // Series removed
|
||||
hasUpdated = true
|
||||
return false
|
||||
}
|
||||
@ -308,7 +299,7 @@ class Server {
|
||||
})
|
||||
}
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', _user)
|
||||
await Database.updateUser(_user)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -321,8 +312,8 @@ class Server {
|
||||
|
||||
getLoginRateLimiter() {
|
||||
return rateLimit({
|
||||
windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes
|
||||
max: this.db.serverSettings.rateLimitLoginRequests,
|
||||
windowMs: Database.serverSettings.rateLimitLoginWindow, // 5 minutes
|
||||
max: Database.serverSettings.rateLimitLoginRequests,
|
||||
skipSuccessfulRequests: true,
|
||||
onLimitReached: this.loginLimitReached
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
const SocketIO = require('socket.io')
|
||||
const Logger = require('./Logger')
|
||||
const Database = require('./Database')
|
||||
|
||||
class SocketAuthority {
|
||||
constructor() {
|
||||
@ -18,7 +19,7 @@ class SocketAuthority {
|
||||
onlineUsersMap[client.user.id].connections++
|
||||
} else {
|
||||
onlineUsersMap[client.user.id] = {
|
||||
...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems),
|
||||
...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems),
|
||||
connections: 1
|
||||
}
|
||||
}
|
||||
@ -107,7 +108,7 @@ class SocketAuthority {
|
||||
delete this.clients[socket.id]
|
||||
} else {
|
||||
Logger.debug('[Server] User Offline ' + _client.user.username)
|
||||
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
|
||||
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems))
|
||||
|
||||
const disconnectTime = Date.now() - _client.connected_at
|
||||
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
|
||||
@ -160,11 +161,11 @@ class SocketAuthority {
|
||||
|
||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||
|
||||
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
|
||||
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, Database.libraryItems))
|
||||
|
||||
// Update user lastSeen
|
||||
user.lastSeen = Date.now()
|
||||
await this.Server.db.updateEntity('user', user)
|
||||
await Database.updateUser(user)
|
||||
|
||||
const initialPayload = {
|
||||
userId: client.user.id,
|
||||
@ -186,7 +187,7 @@ class SocketAuthority {
|
||||
|
||||
if (client.user) {
|
||||
Logger.debug('[Server] User Offline ' + client.user.username)
|
||||
this.adminEmitter('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems))
|
||||
this.adminEmitter('user_offline', client.user.toJSONForPublic(null, Database.libraryItems))
|
||||
}
|
||||
|
||||
delete this.clients[socketId].user
|
||||
|
@ -4,6 +4,7 @@ const { createNewSortInstance } = require('../libs/fastSort')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
|
||||
@ -21,7 +22,7 @@ class AuthorController {
|
||||
|
||||
// Used on author landing page to include library items and items grouped in series
|
||||
if (include.includes('items')) {
|
||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
||||
authorJson.libraryItems = Database.libraryItems.filter(li => {
|
||||
if (libraryId && li.libraryId !== libraryId) return false
|
||||
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
@ -97,23 +98,29 @@ class AuthorController {
|
||||
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||
|
||||
// Check if author name matches another author and merge the authors
|
||||
const existingAuthor = authorNameUpdate ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||
if (existingAuthor) {
|
||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
const bookAuthorsToCreate = []
|
||||
const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||
bookAuthorsToCreate.push({
|
||||
bookId: libraryItem.media.id,
|
||||
authorId: existingAuthor.id
|
||||
})
|
||||
})
|
||||
if (itemsWithAuthor.length) {
|
||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||
await Database.removeBulkBookAuthors(req.author.id) // Remove all old BookAuthor
|
||||
await Database.createBulkBookAuthors(bookAuthorsToCreate) // Create all new BookAuthor
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
// Remove old author
|
||||
await this.db.removeEntity('author', req.author.id)
|
||||
await Database.removeAuthor(req.author.id)
|
||||
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
||||
|
||||
// Send updated num books for merged author
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
const numBooks = Database.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
|
||||
}).length
|
||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||
@ -131,18 +138,17 @@ class AuthorController {
|
||||
req.author.updatedAt = Date.now()
|
||||
|
||||
if (authorNameUpdate) { // Update author name on all books
|
||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
const itemsWithAuthor = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
itemsWithAuthor.forEach(libraryItem => {
|
||||
libraryItem.media.metadata.updateAuthor(req.author)
|
||||
})
|
||||
if (itemsWithAuthor.length) {
|
||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.updateEntity('author', req.author)
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
await Database.updateAuthor(req.author)
|
||||
const numBooks = Database.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
}).length
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||
@ -159,7 +165,7 @@ class AuthorController {
|
||||
var q = (req.query.q || '').toLowerCase()
|
||||
if (!q) return res.json([])
|
||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||
var authors = this.db.authors.filter(au => au.name.toLowerCase().includes(q))
|
||||
var authors = Database.authors.filter(au => au.name.toLowerCase().includes(q))
|
||||
authors = authors.slice(0, limit)
|
||||
res.json({
|
||||
results: authors
|
||||
@ -204,8 +210,8 @@ class AuthorController {
|
||||
if (hasUpdates) {
|
||||
req.author.updatedAt = Date.now()
|
||||
|
||||
await this.db.updateEntity('author', req.author)
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
await Database.updateAuthor(req.author)
|
||||
const numBooks = Database.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
}).length
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||
@ -238,7 +244,7 @@ class AuthorController {
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var author = this.db.authors.find(au => au.id === req.params.id)
|
||||
var author = Database.authors.find(au => au.id === req.params.id)
|
||||
if (!author) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const Collection = require('../objects/Collection')
|
||||
|
||||
@ -13,22 +14,22 @@ class CollectionController {
|
||||
if (!success) {
|
||||
return res.status(500).send('Invalid collection data')
|
||||
}
|
||||
var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('collection', newCollection)
|
||||
var jsonExpanded = newCollection.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createCollection(newCollection)
|
||||
SocketAuthority.emitter('collection_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
findAll(req, res) {
|
||||
res.json({
|
||||
collections: this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems))
|
||||
collections: Database.collections.map(c => c.toJSONExpanded(Database.libraryItems))
|
||||
})
|
||||
}
|
||||
|
||||
findOne(req, res) {
|
||||
const includeEntities = (req.query.include || '').split(',')
|
||||
|
||||
const collectionExpanded = req.collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
if (includeEntities.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||
@ -41,9 +42,9 @@ class CollectionController {
|
||||
async update(req, res) {
|
||||
const collection = req.collection
|
||||
const wasUpdated = collection.update(req.body)
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
await Database.updateCollection(collection)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
@ -51,19 +52,19 @@ class CollectionController {
|
||||
|
||||
async delete(req, res) {
|
||||
const collection = req.collection
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Close rss feed - remove from db and emit socket event
|
||||
await this.rssFeedManager.closeFeedForEntityId(collection.id)
|
||||
|
||||
await this.db.removeEntity('collection', collection.id)
|
||||
await Database.removeCollection(collection.id)
|
||||
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async addBook(req, res) {
|
||||
const collection = req.collection
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === req.body.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(500).send('Book not found')
|
||||
}
|
||||
@ -74,8 +75,14 @@ class CollectionController {
|
||||
return res.status(500).send('Book already in collection')
|
||||
}
|
||||
collection.addBook(req.body.id)
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
const collectionBook = {
|
||||
collectionId: collection.id,
|
||||
bookId: libraryItem.media.id,
|
||||
order: collection.books.length
|
||||
}
|
||||
await Database.createCollectionBook(collectionBook)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@ -83,13 +90,18 @@ class CollectionController {
|
||||
// DELETE: api/collections/:id/book/:bookId
|
||||
async removeBook(req, res) {
|
||||
const collection = req.collection
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === req.params.bookId)
|
||||
if (!libraryItem) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (collection.books.includes(req.params.bookId)) {
|
||||
collection.removeBook(req.params.bookId)
|
||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
await Database.updateCollection(collection)
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// POST: api/collections/:id/batch/add
|
||||
@ -98,19 +110,30 @@ class CollectionController {
|
||||
if (!req.body.books || !req.body.books.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
var bookIdsToAdd = req.body.books
|
||||
var hasUpdated = false
|
||||
for (let i = 0; i < bookIdsToAdd.length; i++) {
|
||||
if (!collection.books.includes(bookIdsToAdd[i])) {
|
||||
collection.addBook(bookIdsToAdd[i])
|
||||
const bookIdsToAdd = req.body.books
|
||||
const collectionBooksToAdd = []
|
||||
let hasUpdated = false
|
||||
|
||||
let order = collection.books.length
|
||||
for (const libraryItemId of bookIdsToAdd) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||
if (!libraryItem) continue
|
||||
if (!collection.books.includes(libraryItemId)) {
|
||||
collection.addBook(libraryItemId)
|
||||
collectionBooksToAdd.push({
|
||||
collectionId: collection.id,
|
||||
bookId: libraryItem.media.id,
|
||||
order: order++
|
||||
})
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.createBulkCollectionBooks(collectionBooksToAdd)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// POST: api/collections/:id/batch/remove
|
||||
@ -120,23 +143,26 @@ class CollectionController {
|
||||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
var bookIdsToRemove = req.body.books
|
||||
var hasUpdated = false
|
||||
for (let i = 0; i < bookIdsToRemove.length; i++) {
|
||||
if (collection.books.includes(bookIdsToRemove[i])) {
|
||||
collection.removeBook(bookIdsToRemove[i])
|
||||
let hasUpdated = false
|
||||
for (const libraryItemId of bookIdsToRemove) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||
if (!libraryItem) continue
|
||||
|
||||
if (collection.books.includes(libraryItemId)) {
|
||||
collection.removeBook(libraryItemId)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.updateCollection(collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
const collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
const collection = Database.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class EmailController {
|
||||
constructor() { }
|
||||
|
||||
getSettings(req, res) {
|
||||
res.json({
|
||||
settings: this.db.emailSettings
|
||||
settings: Database.emailSettings
|
||||
})
|
||||
}
|
||||
|
||||
async updateSettings(req, res) {
|
||||
const updated = this.db.emailSettings.update(req.body)
|
||||
const updated = Database.emailSettings.update(req.body)
|
||||
if (updated) {
|
||||
await this.db.updateEntity('settings', this.db.emailSettings)
|
||||
await Database.updateSetting(Database.emailSettings)
|
||||
}
|
||||
res.json({
|
||||
settings: this.db.emailSettings
|
||||
settings: Database.emailSettings
|
||||
})
|
||||
}
|
||||
|
||||
@ -36,24 +37,24 @@ class EmailController {
|
||||
}
|
||||
}
|
||||
|
||||
const updated = this.db.emailSettings.update({
|
||||
const updated = Database.emailSettings.update({
|
||||
ereaderDevices
|
||||
})
|
||||
if (updated) {
|
||||
await this.db.updateEntity('settings', this.db.emailSettings)
|
||||
await Database.updateSetting(Database.emailSettings)
|
||||
SocketAuthority.adminEmitter('ereader-devices-updated', {
|
||||
ereaderDevices: this.db.emailSettings.ereaderDevices
|
||||
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||
})
|
||||
}
|
||||
res.json({
|
||||
ereaderDevices: this.db.emailSettings.ereaderDevices
|
||||
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||
})
|
||||
}
|
||||
|
||||
async sendEBookToDevice(req, res) {
|
||||
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
||||
|
||||
const libraryItem = this.db.getLibraryItem(req.body.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(req.body.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Library item not found')
|
||||
}
|
||||
@ -67,7 +68,7 @@ class EmailController {
|
||||
return res.status(404).send('EBook file not found')
|
||||
}
|
||||
|
||||
const device = this.db.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||
const device = Database.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||
if (!device) {
|
||||
return res.status(404).send('E-reader device not found')
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
class FileSystemController {
|
||||
@ -16,7 +17,7 @@ class FileSystemController {
|
||||
})
|
||||
|
||||
// Do not include existing mapped library paths in response
|
||||
this.db.libraries.forEach(lib => {
|
||||
Database.libraries.forEach(lib => {
|
||||
lib.folders.forEach((folder) => {
|
||||
let dir = folder.fullPath
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
|
@ -9,6 +9,9 @@ const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
||||
const Database = require('../Database')
|
||||
|
||||
class LibraryController {
|
||||
constructor() { }
|
||||
|
||||
@ -40,13 +43,13 @@ class LibraryController {
|
||||
}
|
||||
|
||||
const library = new Library()
|
||||
newLibraryPayload.displayOrder = this.db.libraries.length + 1
|
||||
newLibraryPayload.displayOrder = Database.libraries.length + 1
|
||||
library.setData(newLibraryPayload)
|
||||
await this.db.insertEntity('library', library)
|
||||
await Database.createLibrary(library)
|
||||
|
||||
// Only emit to users with access to library
|
||||
const userFilter = (user) => {
|
||||
return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id)
|
||||
return user.checkCanAccessLibrary?.(library.id)
|
||||
}
|
||||
SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
|
||||
|
||||
@ -58,14 +61,15 @@ class LibraryController {
|
||||
|
||||
findAll(req, res) {
|
||||
const librariesAccessible = req.user.librariesAccessible || []
|
||||
if (librariesAccessible && librariesAccessible.length) {
|
||||
if (librariesAccessible.length) {
|
||||
return res.json({
|
||||
libraries: this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
||||
libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||
libraries: Database.libraries.map(lib => lib.toJSON())
|
||||
// libraries: Database.libraries.map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
@ -75,7 +79,7 @@ class LibraryController {
|
||||
return res.json({
|
||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
||||
numUserPlaylists: this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
|
||||
numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
|
||||
library: req.library
|
||||
})
|
||||
}
|
||||
@ -128,14 +132,14 @@ class LibraryController {
|
||||
this.cronManager.updateLibraryScanCron(library)
|
||||
|
||||
// Remove libraryItems no longer in library
|
||||
const itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||
const itemsToRemove = Database.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||
if (itemsToRemove.length) {
|
||||
Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`)
|
||||
for (let i = 0; i < itemsToRemove.length; i++) {
|
||||
await this.handleDeleteLibraryItem(itemsToRemove[i])
|
||||
}
|
||||
}
|
||||
await this.db.updateEntity('library', library)
|
||||
await Database.updateLibrary(library)
|
||||
|
||||
// Only emit to users with access to library
|
||||
const userFilter = (user) => {
|
||||
@ -153,21 +157,21 @@ class LibraryController {
|
||||
this.watcher.removeLibrary(library)
|
||||
|
||||
// Remove collections for library
|
||||
const collections = this.db.collections.filter(c => c.libraryId === library.id)
|
||||
const collections = Database.collections.filter(c => c.libraryId === library.id)
|
||||
for (const collection of collections) {
|
||||
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
|
||||
await this.db.removeEntity('collection', collection.id)
|
||||
await Database.removeCollection(collection.id)
|
||||
}
|
||||
|
||||
// Remove items in this library
|
||||
const libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
||||
const libraryItems = Database.libraryItems.filter(li => li.libraryId === library.id)
|
||||
Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`)
|
||||
for (let i = 0; i < libraryItems.length; i++) {
|
||||
await this.handleDeleteLibraryItem(libraryItems[i])
|
||||
}
|
||||
|
||||
const libraryJson = library.toJSON()
|
||||
await this.db.removeEntity('library', library.id)
|
||||
await Database.removeLibrary(library.id)
|
||||
SocketAuthority.emitter('library_removed', libraryJson)
|
||||
return res.json(libraryJson)
|
||||
}
|
||||
@ -209,7 +213,7 @@ class LibraryController {
|
||||
// If also filtering by series, will not collapse the filtered series as this would lead
|
||||
// to series having a collapsed series that is just that series.
|
||||
if (payload.collapseseries) {
|
||||
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries, req.library.settings.hideSingleBookSeries)
|
||||
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, Database.series, filterSeries, req.library.settings.hideSingleBookSeries)
|
||||
|
||||
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
||||
libraryItems = collapsedItems
|
||||
@ -237,7 +241,7 @@ class LibraryController {
|
||||
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||
sortArray.push({
|
||||
asc: (li) => {
|
||||
if (this.db.serverSettings.sortingIgnorePrefix) {
|
||||
if (Database.serverSettings.sortingIgnorePrefix) {
|
||||
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
||||
} else {
|
||||
return li.collapsedSeries?.name || li.media.metadata.title
|
||||
@ -255,7 +259,7 @@ class LibraryController {
|
||||
|
||||
// Handle server setting sortingIgnorePrefix
|
||||
const sortByTitle = sortKey === 'media.metadata.title'
|
||||
if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) {
|
||||
if (sortByTitle && Database.serverSettings.sortingIgnorePrefix) {
|
||||
// BookMetadata.js has titleIgnorePrefix getter
|
||||
sortKey += 'IgnorePrefix'
|
||||
}
|
||||
@ -267,7 +271,7 @@ class LibraryController {
|
||||
sortArray.push({
|
||||
asc: (li) => {
|
||||
if (li.collapsedSeries) {
|
||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||
return Database.serverSettings.sortingIgnorePrefix ?
|
||||
li.collapsedSeries.nameIgnorePrefix :
|
||||
li.collapsedSeries.name
|
||||
} else {
|
||||
@ -284,7 +288,7 @@ class LibraryController {
|
||||
if (mediaIsBook && sortBySequence) {
|
||||
return li.media.metadata.getSeries(filterSeries).sequence
|
||||
} else if (mediaIsBook && sortByTitle && li.collapsedSeries) {
|
||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||
return Database.serverSettings.sortingIgnorePrefix ?
|
||||
li.collapsedSeries.nameIgnorePrefix :
|
||||
li.collapsedSeries.name
|
||||
} else {
|
||||
@ -405,7 +409,7 @@ class LibraryController {
|
||||
include: include.join(',')
|
||||
}
|
||||
|
||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries)
|
||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, Database.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries)
|
||||
|
||||
const direction = payload.sortDesc ? 'desc' : 'asc'
|
||||
series = naturalSort(series).by([
|
||||
@ -422,7 +426,7 @@ class LibraryController {
|
||||
} else if (payload.sortBy === 'lastBookAdded') {
|
||||
return Math.max(...(se.books).map(x => x.addedAt), 0)
|
||||
} else { // sort by name
|
||||
return this.db.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||
return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -466,7 +470,7 @@ class LibraryController {
|
||||
include: include.join(',')
|
||||
}
|
||||
|
||||
let collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
||||
let collections = Database.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
||||
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||
|
||||
// If all books restricted to user in this collection then hide this collection
|
||||
@ -493,7 +497,7 @@ class LibraryController {
|
||||
|
||||
// api/libraries/:id/playlists
|
||||
async getUserPlaylistsForLibrary(req, res) {
|
||||
let playlistsForUser = this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
||||
let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems))
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
@ -517,7 +521,7 @@ class LibraryController {
|
||||
return res.status(400).send('Invalid library media type')
|
||||
}
|
||||
|
||||
let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||
let libraryItems = Database.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||
let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems)
|
||||
albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
|
||||
|
||||
@ -561,26 +565,26 @@ class LibraryController {
|
||||
var orderdata = req.body
|
||||
var hasUpdates = false
|
||||
for (let i = 0; i < orderdata.length; i++) {
|
||||
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
||||
var library = Database.libraries.find(lib => lib.id === orderdata[i].id)
|
||||
if (!library) {
|
||||
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
||||
hasUpdates = true
|
||||
await this.db.updateEntity('library', library)
|
||||
await Database.updateLibrary(library)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
this.db.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
Logger.debug(`[LibraryController] Updated library display orders`)
|
||||
} else {
|
||||
Logger.debug(`[LibraryController] Library orders were up to date`)
|
||||
}
|
||||
|
||||
res.json({
|
||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||
libraries: Database.libraries.map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
@ -610,7 +614,7 @@ class LibraryController {
|
||||
if (queryResult.series?.length) {
|
||||
queryResult.series.forEach((se) => {
|
||||
if (!seriesMatches[se.id]) {
|
||||
const _series = this.db.series.find(_se => _se.id === se.id)
|
||||
const _series = Database.series.find(_se => _se.id === se.id)
|
||||
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
||||
} else {
|
||||
seriesMatches[se.id].books.push(li.toJSON())
|
||||
@ -620,7 +624,7 @@ class LibraryController {
|
||||
if (queryResult.authors?.length) {
|
||||
queryResult.authors.forEach((au) => {
|
||||
if (!authorMatches[au.id]) {
|
||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authorMatches[au.id] = _author.toJSON()
|
||||
authorMatches[au.id].numBooks = 1
|
||||
@ -687,7 +691,7 @@ class LibraryController {
|
||||
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
||||
li.media.metadata.authors.forEach((au) => {
|
||||
if (!authors[au.id]) {
|
||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authors[au.id] = _author.toJSON()
|
||||
authors[au.id].numBooks = 1
|
||||
@ -749,7 +753,7 @@ class LibraryController {
|
||||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
@ -774,7 +778,7 @@ class LibraryController {
|
||||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
@ -858,12 +862,12 @@ class LibraryController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||
const library = Database.libraries.find(lib => lib.id === req.params.id)
|
||||
if (!library) {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
req.library = library
|
||||
req.libraryItems = this.db.libraryItems.filter(li => {
|
||||
req.libraryItems = Database.libraryItems.filter(li => {
|
||||
return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li)
|
||||
})
|
||||
next()
|
||||
|
@ -2,9 +2,10 @@ const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const zipHelpers = require('../utils/zipHelpers')
|
||||
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { ScanResult } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||
|
||||
@ -31,7 +32,7 @@ class LibraryItemController {
|
||||
if (item.mediaType == 'book') {
|
||||
if (includeEntities.includes('authors')) {
|
||||
item.media.metadata.authors = item.media.metadata.authors.map(au => {
|
||||
var author = this.db.authors.find(_au => _au.id === au.id)
|
||||
var author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (!author) return null
|
||||
return {
|
||||
...author
|
||||
@ -61,7 +62,7 @@ class LibraryItemController {
|
||||
const hasUpdates = libraryItem.update(req.body)
|
||||
if (hasUpdates) {
|
||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json(libraryItem.toJSON())
|
||||
@ -139,7 +140,7 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json({
|
||||
@ -174,7 +175,7 @@ class LibraryItemController {
|
||||
return res.status(500).send('Unknown error occurred')
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json({
|
||||
success: true,
|
||||
@ -194,7 +195,7 @@ class LibraryItemController {
|
||||
return res.status(500).send(validationResult.error)
|
||||
}
|
||||
if (validationResult.updated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json({
|
||||
@ -210,7 +211,7 @@ class LibraryItemController {
|
||||
if (libraryItem.media.coverPath) {
|
||||
libraryItem.updateMediaCover('')
|
||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -282,7 +283,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
@ -309,7 +310,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||
const itemsToDelete = Database.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||
if (!itemsToDelete.length) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
@ -338,7 +339,7 @@ class LibraryItemController {
|
||||
|
||||
for (let i = 0; i < updatePayloads.length; i++) {
|
||||
var mediaPayload = updatePayloads[i].mediaPayload
|
||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||
var libraryItem = Database.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||
if (!libraryItem) return null
|
||||
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||
@ -346,7 +347,7 @@ class LibraryItemController {
|
||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||
if (hasUpdates) {
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
itemsUpdated++
|
||||
}
|
||||
@ -366,7 +367,7 @@ class LibraryItemController {
|
||||
}
|
||||
const libraryItems = []
|
||||
libraryItemIds.forEach((lid) => {
|
||||
const li = this.db.libraryItems.find(_li => _li.id === lid)
|
||||
const li = Database.libraryItems.find(_li => _li.id === lid)
|
||||
if (li) libraryItems.push(li.toJSONExpanded())
|
||||
})
|
||||
res.json({
|
||||
@ -389,7 +390,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
@ -424,7 +425,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
@ -441,15 +442,17 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
// DELETE: api/items/all
|
||||
// TODO: Remove
|
||||
async deleteAll(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.warn('User other than admin attempted to delete all library items', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info('Removing all Library Items')
|
||||
var success = await this.db.recreateLibraryItemsDb()
|
||||
if (success) res.sendStatus(200)
|
||||
else res.sendStatus(500)
|
||||
return res.sendStatus(404)
|
||||
// if (!req.user.isAdminOrUp) {
|
||||
// Logger.warn('User other than admin attempted to delete all library items', req.user)
|
||||
// return res.sendStatus(403)
|
||||
// }
|
||||
// Logger.info('Removing all Library Items')
|
||||
// var success = await this.db.recreateLibraryItemsDb()
|
||||
// if (success) res.sendStatus(200)
|
||||
// else res.sendStatus(500)
|
||||
}
|
||||
|
||||
// POST: api/items/:id/scan (admin)
|
||||
@ -504,7 +507,7 @@ class LibraryItemController {
|
||||
const chapters = req.body.chapters || []
|
||||
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -586,7 +589,7 @@ class LibraryItemController {
|
||||
}
|
||||
}
|
||||
req.libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@ -682,13 +685,13 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
req.libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
req.libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
@ -1,7 +1,8 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const { sort } = require('../libs/fastSort')
|
||||
const { isObject, toNumber } = require('../utils/index')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class MeController {
|
||||
constructor() { }
|
||||
@ -33,7 +34,7 @@ class MeController {
|
||||
|
||||
// GET: api/me/listening-stats
|
||||
async getListeningStats(req, res) {
|
||||
var listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||
res.json(listeningStats)
|
||||
}
|
||||
|
||||
@ -51,21 +52,21 @@ class MeController {
|
||||
if (!req.user.removeMediaProgress(req.params.id)) {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.removeMediaProgress(req.params.id)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// PATCH: api/me/progress/:id
|
||||
async createUpdateMediaProgress(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, req.body)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.sendStatus(200)
|
||||
@ -73,8 +74,8 @@ class MeController {
|
||||
|
||||
// PATCH: api/me/progress/:id/:episodeId
|
||||
async createUpdateEpisodeMediaProgress(req, res) {
|
||||
var episodeId = req.params.episodeId
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
@ -83,9 +84,9 @@ class MeController {
|
||||
return res.status(404).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.sendStatus(200)
|
||||
@ -93,24 +94,26 @@ class MeController {
|
||||
|
||||
// PATCH: api/me/progress/batch/update
|
||||
async batchUpdateMediaProgress(req, res) {
|
||||
var itemProgressPayloads = req.body
|
||||
if (!itemProgressPayloads || !itemProgressPayloads.length) {
|
||||
const itemProgressPayloads = req.body
|
||||
if (!itemProgressPayloads?.length) {
|
||||
return res.status(400).send('Missing request payload')
|
||||
}
|
||||
|
||||
var shouldUpdate = false
|
||||
itemProgressPayloads.forEach((itemProgress) => {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
let shouldUpdate = false
|
||||
for (const itemProgress of itemProgressPayloads) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
if (libraryItem) {
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)
|
||||
if (wasUpdated) shouldUpdate = true
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
shouldUpdate = true
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
@ -119,18 +122,18 @@ class MeController {
|
||||
|
||||
// POST: api/me/item/:id/bookmark
|
||||
async createBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
const { time, title } = req.body
|
||||
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
}
|
||||
|
||||
// PATCH: api/me/item/:id/bookmark
|
||||
async updateBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
const { time, title } = req.body
|
||||
if (!req.user.findBookmark(libraryItem.id, time)) {
|
||||
@ -139,14 +142,14 @@ class MeController {
|
||||
}
|
||||
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
|
||||
if (!bookmark) return res.sendStatus(500)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
}
|
||||
|
||||
// DELETE: api/me/item/:id/bookmark/:time
|
||||
async removeBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
var time = Number(req.params.time)
|
||||
if (isNaN(time)) return res.sendStatus(500)
|
||||
@ -156,7 +159,7 @@ class MeController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
req.user.removeBookmark(libraryItem.id, time)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@ -178,16 +181,16 @@ class MeController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
const updatedLocalMediaProgress = []
|
||||
var numServerProgressUpdates = 0
|
||||
let numServerProgressUpdates = 0
|
||||
const updatedServerMediaProgress = []
|
||||
const localMediaProgress = req.body.localMediaProgress || []
|
||||
|
||||
localMediaProgress.forEach(localProgress => {
|
||||
for (const localProgress of localMediaProgress) {
|
||||
if (!localProgress.libraryItemId) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
||||
return
|
||||
}
|
||||
var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(localProgress.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
||||
return
|
||||
@ -199,12 +202,14 @@ class MeController {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
||||
@ -222,11 +227,10 @@ class MeController {
|
||||
} else {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
|
||||
if (numServerProgressUpdates > 0) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
@ -244,7 +248,7 @@ class MeController {
|
||||
let itemsInProgress = []
|
||||
for (const mediaProgress of req.user.mediaProgress) {
|
||||
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||
const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(mediaProgress.libraryItemId)
|
||||
if (libraryItem) {
|
||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||
@ -274,7 +278,7 @@ class MeController {
|
||||
|
||||
// GET: api/me/series/:id/remove-from-continue-listening
|
||||
async removeSeriesFromContinueListening(req, res) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
@ -282,7 +286,7 @@ class MeController {
|
||||
|
||||
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
@ -290,7 +294,7 @@ class MeController {
|
||||
|
||||
// GET: api/me/series/:id/readd-to-continue-listening
|
||||
async readdSeriesFromContinueListening(req, res) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
@ -298,7 +302,7 @@ class MeController {
|
||||
|
||||
const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
@ -308,7 +312,7 @@ class MeController {
|
||||
async removeItemFromContinueListening(req, res) {
|
||||
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
|
@ -2,6 +2,7 @@ const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const filePerms = require('../utils/filePerms')
|
||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||
@ -30,7 +31,7 @@ class MiscController {
|
||||
var libraryId = req.body.library
|
||||
var folderId = req.body.folder
|
||||
|
||||
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||
var library = Database.libraries.find(lib => lib.id === libraryId)
|
||||
if (!library) {
|
||||
return res.status(404).send(`Library not found with id ${libraryId}`)
|
||||
}
|
||||
@ -116,18 +117,18 @@ class MiscController {
|
||||
return res.status(500).send('Invalid settings update object')
|
||||
}
|
||||
|
||||
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
||||
var madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||
if (madeUpdates) {
|
||||
// If backup schedule is updated - update backup manager
|
||||
if (settingsUpdate.backupSchedule !== undefined) {
|
||||
this.backupManager.updateCronSchedule()
|
||||
}
|
||||
|
||||
await this.db.updateServerSettings()
|
||||
await Database.updateServerSettings()
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
serverSettings: this.db.serverSettings.toJSONForBrowser()
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||
})
|
||||
}
|
||||
|
||||
@ -147,7 +148,7 @@ class MiscController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
const tags = []
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.media.tags && li.media.tags.length) {
|
||||
li.media.tags.forEach((tag) => {
|
||||
if (!tags.includes(tag)) tags.push(tag)
|
||||
@ -176,7 +177,7 @@ class MiscController {
|
||||
let tagMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
|
||||
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
||||
@ -187,7 +188,7 @@ class MiscController {
|
||||
li.media.tags.push(newTag) // Add new tag
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
@ -209,13 +210,13 @@ class MiscController {
|
||||
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
|
||||
if (li.media.tags.includes(tag)) {
|
||||
li.media.tags = li.media.tags.filter(t => t !== tag)
|
||||
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
@ -233,7 +234,7 @@ class MiscController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
const genres = []
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.media.metadata.genres && li.media.metadata.genres.length) {
|
||||
li.media.metadata.genres.forEach((genre) => {
|
||||
if (!genres.includes(genre)) genres.push(genre)
|
||||
@ -262,7 +263,7 @@ class MiscController {
|
||||
let genreMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
|
||||
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
||||
@ -273,7 +274,7 @@ class MiscController {
|
||||
li.media.metadata.genres.push(newGenre) // Add new genre
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
@ -295,13 +296,13 @@ class MiscController {
|
||||
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
|
||||
if (li.media.metadata.genres.includes(genre)) {
|
||||
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre)
|
||||
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { version } = require('../../package.json')
|
||||
|
||||
class NotificationController {
|
||||
@ -7,14 +8,14 @@ class NotificationController {
|
||||
get(req, res) {
|
||||
res.json({
|
||||
data: this.notificationManager.getData(),
|
||||
settings: this.db.notificationSettings
|
||||
settings: Database.notificationSettings
|
||||
})
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
const updated = this.db.notificationSettings.update(req.body)
|
||||
const updated = Database.notificationSettings.update(req.body)
|
||||
if (updated) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@ -29,31 +30,31 @@ class NotificationController {
|
||||
}
|
||||
|
||||
async createNotification(req, res) {
|
||||
const success = this.db.notificationSettings.createNotification(req.body)
|
||||
const success = Database.notificationSettings.createNotification(req.body)
|
||||
|
||||
if (success) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async deleteNotification(req, res) {
|
||||
if (this.db.notificationSettings.removeNotification(req.notification.id)) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
if (Database.notificationSettings.removeNotification(req.notification.id)) {
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async updateNotification(req, res) {
|
||||
const success = this.db.notificationSettings.updateNotification(req.body)
|
||||
const success = Database.notificationSettings.updateNotification(req.body)
|
||||
if (success) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async sendNotificationTest(req, res) {
|
||||
if (!this.db.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
||||
if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
||||
|
||||
const success = await this.notificationManager.sendTestNotification(req.notification)
|
||||
if (success) res.sendStatus(200)
|
||||
@ -66,7 +67,7 @@ class NotificationController {
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const notification = this.db.notificationSettings.getNotification(req.params.id)
|
||||
const notification = Database.notificationSettings.getNotification(req.params.id)
|
||||
if (!notification) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const Playlist = require('../objects/Playlist')
|
||||
|
||||
@ -14,8 +15,8 @@ class PlaylistController {
|
||||
if (!success) {
|
||||
return res.status(400).send('Invalid playlist request data')
|
||||
}
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('playlist', newPlaylist)
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@ -23,22 +24,22 @@ class PlaylistController {
|
||||
// GET: api/playlists
|
||||
findAllForUser(req, res) {
|
||||
res.json({
|
||||
playlists: this.db.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
||||
playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems))
|
||||
})
|
||||
}
|
||||
|
||||
// GET: api/playlists/:id
|
||||
findOne(req, res) {
|
||||
res.json(req.playlist.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(req.playlist.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// PATCH: api/playlists/:id
|
||||
async update(req, res) {
|
||||
const playlist = req.playlist
|
||||
let wasUpdated = playlist.update(req.body)
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
@ -47,8 +48,8 @@ class PlaylistController {
|
||||
// DELETE: api/playlists/:id
|
||||
async delete(req, res) {
|
||||
const playlist = req.playlist
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@ -62,7 +63,7 @@ class PlaylistController {
|
||||
return res.status(400).send('Request body has no libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(400).send('Library item not found')
|
||||
}
|
||||
@ -80,8 +81,16 @@ class PlaylistController {
|
||||
}
|
||||
|
||||
playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId)
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
|
||||
const playlistMediaItem = {
|
||||
playlistId: playlist.id,
|
||||
mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
|
||||
mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: playlist.items.length
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylistMediaItem(playlistMediaItem)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
@ -99,15 +108,15 @@ class PlaylistController {
|
||||
|
||||
playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId)
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Playlist is removed when there are no items
|
||||
if (!playlist.items.length) {
|
||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
|
||||
@ -122,20 +131,34 @@ class PlaylistController {
|
||||
}
|
||||
const itemsToAdd = req.body.items
|
||||
let hasUpdated = false
|
||||
|
||||
let order = playlist.items.length
|
||||
const playlistMediaItems = []
|
||||
for (const item of itemsToAdd) {
|
||||
if (!item.libraryItemId) {
|
||||
return res.status(400).send('Item does not have libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = Database.getLibraryItem(item.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(400).send('Item not found with id ' + item.libraryItemId)
|
||||
}
|
||||
|
||||
if (!playlist.containsItem(item)) {
|
||||
playlistMediaItems.push({
|
||||
playlistId: playlist.id,
|
||||
mediaItemId: item.episodeId || libraryItem.media.id, // podcastEpisodeId or bookId
|
||||
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: order++
|
||||
})
|
||||
playlist.addItem(item.libraryItemId, item.episodeId)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
@ -153,21 +176,22 @@ class PlaylistController {
|
||||
if (!item.libraryItemId) {
|
||||
return res.status(400).send('Item does not have libraryItemId')
|
||||
}
|
||||
|
||||
if (playlist.containsItem(item)) {
|
||||
playlist.removeItem(item.libraryItemId, item.episodeId)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (hasUpdated) {
|
||||
// Playlist is removed when there are no items
|
||||
if (!playlist.items.length) {
|
||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
}
|
||||
@ -176,12 +200,12 @@ class PlaylistController {
|
||||
|
||||
// POST: api/playlists/collection/:collectionId
|
||||
async createFromCollection(req, res) {
|
||||
let collection = this.db.collections.find(c => c.id === req.params.collectionId)
|
||||
let collection = Database.collections.find(c => c.id === req.params.collectionId)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
// Expand collection to get library items
|
||||
collection = collection.toJSONExpanded(this.db.libraryItems)
|
||||
collection = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Filter out library items not accessible to user
|
||||
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||
@ -201,15 +225,15 @@ class PlaylistController {
|
||||
}
|
||||
newPlaylist.setData(newPlaylistData)
|
||||
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('playlist', newPlaylist)
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
const playlist = this.db.playlists.find(p => p.id === req.params.id)
|
||||
const playlist = Database.playlists.find(p => p.id === req.params.id)
|
||||
if (!playlist) {
|
||||
return res.status(404).send('Playlist not found')
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
@ -18,7 +19,7 @@ class PodcastController {
|
||||
}
|
||||
const payload = req.body
|
||||
|
||||
const library = this.db.libraries.find(lib => lib.id === payload.libraryId)
|
||||
const library = Database.libraries.find(lib => lib.id === payload.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||
return res.status(404).send('Library not found')
|
||||
@ -33,7 +34,7 @@ class PodcastController {
|
||||
const podcastPath = filePathToPOSIX(payload.path)
|
||||
|
||||
// Check if a library item with this podcast folder exists already
|
||||
const existingLibraryItem = this.db.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||
const existingLibraryItem = Database.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||
if (existingLibraryItem) {
|
||||
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
||||
return res.status(400).send('Podcast already exists')
|
||||
@ -80,7 +81,7 @@ class PodcastController {
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.insertLibraryItem(libraryItem)
|
||||
await Database.createLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
|
||||
|
||||
res.json(libraryItem.toJSONExpanded())
|
||||
@ -199,7 +200,7 @@ class PodcastController {
|
||||
const overrideDetails = req.query.override === '1'
|
||||
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||
if (episodesUpdated) {
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -216,9 +217,8 @@ class PodcastController {
|
||||
return res.status(404).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
if (libraryItem.media.updateEpisode(episodeId, req.body)) {
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -267,13 +267,13 @@ class PodcastController {
|
||||
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
if (!item.isPodcast) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class RSSFeedController {
|
||||
constructor() { }
|
||||
@ -8,7 +8,7 @@ class RSSFeedController {
|
||||
async openRSSFeedForItem(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.itemId)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.itemId)
|
||||
if (!item) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
@ -45,7 +45,7 @@ class RSSFeedController {
|
||||
async openRSSFeedForCollection(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const collection = this.db.collections.find(li => li.id === req.params.collectionId)
|
||||
const collection = Database.collections.find(li => li.id === req.params.collectionId)
|
||||
if (!collection) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
@ -60,7 +60,7 @@ class RSSFeedController {
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||
|
||||
// Check collection has audio tracks
|
||||
@ -79,7 +79,7 @@ class RSSFeedController {
|
||||
async openRSSFeedForSeries(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const series = this.db.series.find(se => se.id === req.params.seriesId)
|
||||
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
@ -96,7 +96,7 @@ class RSSFeedController {
|
||||
|
||||
const seriesJson = series.toJSON()
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
|
||||
// Check series has audio tracks
|
||||
if (!seriesJson.books.length) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class SeriesController {
|
||||
constructor() { }
|
||||
@ -35,7 +36,7 @@ class SeriesController {
|
||||
var q = (req.query.q || '').toLowerCase()
|
||||
if (!q) return res.json([])
|
||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||
var series = this.db.series.filter(se => se.name.toLowerCase().includes(q))
|
||||
var series = Database.series.filter(se => se.name.toLowerCase().includes(q))
|
||||
series = series.slice(0, limit)
|
||||
res.json({
|
||||
results: series
|
||||
@ -45,17 +46,17 @@ class SeriesController {
|
||||
async update(req, res) {
|
||||
const hasUpdated = req.series.update(req.body)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('series', req.series)
|
||||
await Database.updateSeries(req.series)
|
||||
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||
}
|
||||
res.json(req.series.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||
const libraryItemsInSeries = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||
if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) {
|
||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user)
|
||||
return res.sendStatus(403)
|
||||
|
@ -1,4 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class SessionController {
|
||||
@ -49,7 +50,7 @@ class SessionController {
|
||||
}
|
||||
|
||||
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
||||
const user = this.db.users.find(u => u.id === se.userId) || null
|
||||
const user = Database.users.find(u => u.id === se.userId) || null
|
||||
return {
|
||||
...se.toJSON(),
|
||||
user: user ? { id: user.id, username: user.username } : null
|
||||
@ -62,7 +63,7 @@ class SessionController {
|
||||
}
|
||||
|
||||
getOpenSession(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||
var libraryItem = Database.getLibraryItem(req.session.libraryItemId)
|
||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
res.json(sessionForClient)
|
||||
}
|
||||
@ -87,7 +88,7 @@ class SessionController {
|
||||
await this.playbackSessionManager.removeSession(req.session.id)
|
||||
}
|
||||
|
||||
await this.db.removeEntity('session', req.session.id)
|
||||
await Database.removePlaybackSession(req.session.id)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@ -115,7 +116,7 @@ class SessionController {
|
||||
}
|
||||
|
||||
async middleware(req, res, next) {
|
||||
const playbackSession = await this.db.getPlaybackSession(req.params.id)
|
||||
const playbackSession = await Database.getPlaybackSession(req.params.id)
|
||||
if (!playbackSession) {
|
||||
Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -1,4 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
|
||||
class ToolsController {
|
||||
constructor() { }
|
||||
@ -65,7 +66,7 @@ class ToolsController {
|
||||
|
||||
const libraryItems = []
|
||||
for (const libraryItemId of libraryItemIds) {
|
||||
const libraryItem = this.db.getLibraryItem(libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
||||
return res.sendStatus(404)
|
||||
@ -105,7 +106,7 @@ class ToolsController {
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
@ -1,9 +1,11 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const User = require('../objects/user/User')
|
||||
|
||||
const { getId, toNumber } = require('../utils/index')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class UserController {
|
||||
constructor() { }
|
||||
@ -15,11 +17,11 @@ class UserController {
|
||||
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
||||
|
||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||
const users = this.db.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||
const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||
|
||||
if (includes.includes('latestSession')) {
|
||||
for (const user of users) {
|
||||
const userSessions = await this.db.selectUserSessions(user.id)
|
||||
const userSessions = await Database.getPlaybackSessions({ userId: user.id })
|
||||
user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null
|
||||
}
|
||||
}
|
||||
@ -35,7 +37,7 @@ class UserController {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const user = this.db.users.find(u => u.id === req.params.id)
|
||||
const user = Database.users.find(u => u.id === req.params.id)
|
||||
if (!user) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
@ -47,18 +49,19 @@ class UserController {
|
||||
var account = req.body
|
||||
|
||||
var username = account.username
|
||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
||||
var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
|
||||
account.id = getId('usr')
|
||||
account.id = uuidv4()
|
||||
account.pash = await this.auth.hashPass(account.password)
|
||||
delete account.password
|
||||
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
|
||||
account.createdAt = Date.now()
|
||||
var newUser = new User(account)
|
||||
var success = await this.db.insertEntity('user', newUser)
|
||||
const newUser = new User(account)
|
||||
|
||||
const success = await Database.createUser(newUser)
|
||||
if (success) {
|
||||
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
||||
res.json({
|
||||
@ -81,7 +84,7 @@ class UserController {
|
||||
var shouldUpdateToken = false
|
||||
|
||||
if (account.username !== undefined && account.username !== user.username) {
|
||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||
var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
@ -94,13 +97,12 @@ class UserController {
|
||||
delete account.password
|
||||
}
|
||||
|
||||
var hasUpdated = user.update(account)
|
||||
if (hasUpdated) {
|
||||
if (user.update(account)) {
|
||||
if (shouldUpdateToken) {
|
||||
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
|
||||
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||
}
|
||||
await this.db.updateEntity('user', user)
|
||||
await Database.updateUser(user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
@ -124,13 +126,13 @@ class UserController {
|
||||
// Todo: check if user is logged in and cancel streams
|
||||
|
||||
// Remove user playlists
|
||||
const userPlaylists = this.db.playlists.filter(p => p.userId === user.id)
|
||||
const userPlaylists = Database.playlists.filter(p => p.userId === user.id)
|
||||
for (const playlist of userPlaylists) {
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
}
|
||||
|
||||
const userJson = user.toJSONForBrowser()
|
||||
await this.db.removeEntity('user', user.id)
|
||||
await Database.removeUser(user.id)
|
||||
SocketAuthority.adminEmitter('user_removed', userJson)
|
||||
res.json({
|
||||
success: true
|
||||
@ -165,37 +167,39 @@ class UserController {
|
||||
}
|
||||
|
||||
// POST: api/users/:id/purge-media-progress
|
||||
// TODO: Remove
|
||||
async purgeMediaProgress(req, res) {
|
||||
const user = req.reqUser
|
||||
return res.sendStatus(404)
|
||||
// const user = req.reqUser
|
||||
|
||||
if (user.type === 'root' && !req.user.isRoot) {
|
||||
Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
// if (user.type === 'root' && !req.user.isRoot) {
|
||||
// Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
||||
// return res.sendStatus(403)
|
||||
// }
|
||||
|
||||
var progressPurged = 0
|
||||
user.mediaProgress = user.mediaProgress.filter(mp => {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
progressPurged++
|
||||
return false
|
||||
} else if (mp.episodeId) {
|
||||
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
||||
if (!episode) { // Episode not found
|
||||
progressPurged++
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
// var progressPurged = 0
|
||||
// user.mediaProgress = user.mediaProgress.filter(mp => {
|
||||
// const libraryItem = Database.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||
// if (!libraryItem) {
|
||||
// progressPurged++
|
||||
// return false
|
||||
// } else if (mp.episodeId) {
|
||||
// const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
||||
// if (!episode) { // Episode not found
|
||||
// progressPurged++
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
|
||||
if (progressPurged) {
|
||||
Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
||||
await this.db.updateEntity('user', user)
|
||||
SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
|
||||
}
|
||||
// if (progressPurged) {
|
||||
// Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
||||
// await this.db.updateEntity('user', user)
|
||||
// SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
|
||||
// }
|
||||
|
||||
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||
// res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||
}
|
||||
|
||||
// POST: api/users/online (admin)
|
||||
@ -218,7 +222,7 @@ class UserController {
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
req.reqUser = this.db.users.find(u => u.id === req.params.id)
|
||||
req.reqUser = Database.users.find(u => u.id === req.params.id)
|
||||
if (!req.reqUser) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
16
server/controllers2/libraryItem.controller.js
Normal file
16
server/controllers2/libraryItem.controller.js
Normal file
@ -0,0 +1,16 @@
|
||||
const itemDb = require('../db/item.db')
|
||||
|
||||
const getLibraryItem = async (req, res) => {
|
||||
let libraryItem = null
|
||||
if (req.query.expanded == 1) {
|
||||
libraryItem = await itemDb.getLibraryItemExpanded(req.params.id)
|
||||
} else {
|
||||
libraryItem = await itemDb.getLibraryItemMinified(req.params.id)
|
||||
}
|
||||
|
||||
res.json(libraryItem)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLibraryItem
|
||||
}
|
80
server/db/libraryItem.db.js
Normal file
80
server/db/libraryItem.db.js
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* TODO: Unused for testing
|
||||
*/
|
||||
const { Sequelize } = require('sequelize')
|
||||
const Database = require('../Database')
|
||||
|
||||
const getLibraryItemMinified = (libraryItemId) => {
|
||||
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||
include: [
|
||||
{
|
||||
model: Database.models.book,
|
||||
attributes: [
|
||||
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags'
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Database.models.author,
|
||||
attributes: ['id', 'name'],
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Database.models.series,
|
||||
attributes: ['id', 'name'],
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Database.models.podcast,
|
||||
attributes: [
|
||||
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags',
|
||||
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const getLibraryItemExpanded = (libraryItemId) => {
|
||||
return Database.models.libraryItem.findByPk(libraryItemId, {
|
||||
include: [
|
||||
{
|
||||
model: Database.models.book,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Database.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: Database.models.podcast,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.podcastEpisode
|
||||
}
|
||||
]
|
||||
},
|
||||
'libraryFolder',
|
||||
'library'
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLibraryItemMinified,
|
||||
getLibraryItemExpanded
|
||||
}
|
@ -10,8 +10,7 @@ const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||
const toneHelpers = require('../utils/toneHelpers')
|
||||
|
||||
class AbMergeManager {
|
||||
constructor(db, taskManager) {
|
||||
this.db = db
|
||||
constructor(taskManager) {
|
||||
this.taskManager = taskManager
|
||||
|
||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||
|
@ -10,8 +10,7 @@ const toneHelpers = require('../utils/toneHelpers')
|
||||
const Task = require('../objects/Task')
|
||||
|
||||
class AudioMetadataMangaer {
|
||||
constructor(db, taskManager) {
|
||||
this.db = db
|
||||
constructor(taskManager) {
|
||||
this.taskManager = taskManager
|
||||
|
||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||
|
@ -10,15 +10,14 @@ const { downloadFile, filePathToPOSIX } = require('../utils/fileUtils')
|
||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||
|
||||
class CoverManager {
|
||||
constructor(db, cacheManager) {
|
||||
this.db = db
|
||||
constructor(cacheManager) {
|
||||
this.cacheManager = cacheManager
|
||||
|
||||
this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items')
|
||||
}
|
||||
|
||||
getCoverDirectory(libraryItem) {
|
||||
if (this.db.serverSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) {
|
||||
if (global.ServerSettings.storeCoverWithItem && !libraryItem.isFile && !libraryItem.isMusic) {
|
||||
return libraryItem.path
|
||||
} else {
|
||||
return Path.posix.join(this.ItemMetadataPath, libraryItem.id)
|
||||
|
@ -1,9 +1,9 @@
|
||||
const cron = require('../libs/nodeCron')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
|
||||
class CronManager {
|
||||
constructor(db, scanner, podcastManager) {
|
||||
this.db = db
|
||||
constructor(scanner, podcastManager) {
|
||||
this.scanner = scanner
|
||||
this.podcastManager = podcastManager
|
||||
|
||||
@ -19,7 +19,7 @@ class CronManager {
|
||||
}
|
||||
|
||||
initLibraryScanCrons() {
|
||||
for (const library of this.db.libraries) {
|
||||
for (const library of Database.libraries) {
|
||||
if (library.settings.autoScanCronExpression) {
|
||||
this.startCronForLibrary(library)
|
||||
}
|
||||
@ -64,7 +64,7 @@ class CronManager {
|
||||
|
||||
initPodcastCrons() {
|
||||
const cronExpressionMap = {}
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.mediaType === 'podcast' && li.media.autoDownloadEpisodes) {
|
||||
if (!li.media.autoDownloadSchedule) {
|
||||
Logger.error(`[CronManager] Podcast auto download schedule is not set for ${li.media.metadata.title}`)
|
||||
@ -119,7 +119,7 @@ class CronManager {
|
||||
// Get podcast library items to check
|
||||
const libraryItems = []
|
||||
for (const libraryItemId of libraryItemIds) {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[CronManager] Library item ${libraryItemId} not found for episode check cron ${expression}`)
|
||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
||||
|
@ -1,14 +1,11 @@
|
||||
const nodemailer = require('nodemailer')
|
||||
const Logger = require("../Logger")
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
|
||||
class EmailManager {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
getTransporter() {
|
||||
return nodemailer.createTransport(this.db.emailSettings.getTransportObject())
|
||||
return nodemailer.createTransport(Database.emailSettings.getTransportObject())
|
||||
}
|
||||
|
||||
async sendTest(res) {
|
||||
@ -25,8 +22,8 @@ class EmailManager {
|
||||
}
|
||||
|
||||
transporter.sendMail({
|
||||
from: this.db.emailSettings.fromAddress,
|
||||
to: this.db.emailSettings.testAddress || this.db.emailSettings.fromAddress,
|
||||
from: Database.emailSettings.fromAddress,
|
||||
to: Database.emailSettings.testAddress || Database.emailSettings.fromAddress,
|
||||
subject: 'Test email from Audiobookshelf',
|
||||
text: 'Success!'
|
||||
}).then((result) => {
|
||||
@ -52,7 +49,7 @@ class EmailManager {
|
||||
}
|
||||
|
||||
transporter.sendMail({
|
||||
from: this.db.emailSettings.fromAddress,
|
||||
from: Database.emailSettings.fromAddress,
|
||||
to: device.email,
|
||||
subject: "Here is your Ebook!",
|
||||
html: '<div dir="auto"></div>',
|
||||
|
@ -9,9 +9,7 @@ const Logger = require('../Logger')
|
||||
const TAG = '[LogManager]'
|
||||
|
||||
class LogManager {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
|
||||
constructor() {
|
||||
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||
|
||||
@ -20,12 +18,8 @@ class LogManager {
|
||||
this.dailyLogFiles = []
|
||||
}
|
||||
|
||||
get serverSettings() {
|
||||
return this.db.serverSettings || {}
|
||||
}
|
||||
|
||||
get loggerDailyLogsToKeep() {
|
||||
return this.serverSettings.loggerDailyLogsToKeep || 7
|
||||
return global.ServerSettings.loggerDailyLogsToKeep || 7
|
||||
}
|
||||
|
||||
async ensureLogDirs() {
|
||||
|
@ -1,12 +1,11 @@
|
||||
const axios = require('axios')
|
||||
const Logger = require("../Logger")
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const { notificationData } = require('../utils/notifications')
|
||||
|
||||
class NotificationManager {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
|
||||
constructor() {
|
||||
this.sendingNotification = false
|
||||
this.notificationQueue = []
|
||||
}
|
||||
@ -16,10 +15,10 @@ class NotificationManager {
|
||||
}
|
||||
|
||||
onPodcastEpisodeDownloaded(libraryItem, episode) {
|
||||
if (!this.db.notificationSettings.isUseable) return
|
||||
if (!Database.notificationSettings.isUseable) return
|
||||
|
||||
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
|
||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||
const eventData = {
|
||||
libraryItemId: libraryItem.id,
|
||||
libraryId: libraryItem.libraryId,
|
||||
@ -42,19 +41,19 @@ class NotificationManager {
|
||||
}
|
||||
|
||||
async triggerNotification(eventName, eventData, intentionallyFail = false) {
|
||||
if (!this.db.notificationSettings.isUseable) return
|
||||
if (!Database.notificationSettings.isUseable) return
|
||||
|
||||
// Will queue the notification if sendingNotification and queue is not full
|
||||
if (!this.checkTriggerNotification(eventName, eventData)) return
|
||||
|
||||
const notifications = this.db.notificationSettings.getActiveNotificationsForEvent(eventName)
|
||||
const notifications = Database.notificationSettings.getActiveNotificationsForEvent(eventName)
|
||||
for (const notification of notifications) {
|
||||
Logger.debug(`[NotificationManager] triggerNotification: Sending ${eventName} notification ${notification.id}`)
|
||||
const success = intentionallyFail ? false : await this.sendNotification(notification, eventData)
|
||||
|
||||
notification.updateNotificationFired(success)
|
||||
if (!success) { // Failed notification
|
||||
if (notification.numConsecutiveFailedAttempts >= this.db.notificationSettings.maxFailedAttempts) {
|
||||
if (notification.numConsecutiveFailedAttempts >= Database.notificationSettings.maxFailedAttempts) {
|
||||
Logger.error(`[NotificationManager] triggerNotification: ${notification.eventName}/${notification.id} reached max failed attempts`)
|
||||
notification.enabled = false
|
||||
} else {
|
||||
@ -63,8 +62,8 @@ class NotificationManager {
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
SocketAuthority.emitter('notifications_updated', this.db.notificationSettings.toJSON())
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
SocketAuthority.emitter('notifications_updated', Database.notificationSettings.toJSON())
|
||||
|
||||
this.notificationFinished()
|
||||
}
|
||||
@ -72,7 +71,7 @@ class NotificationManager {
|
||||
// Return TRUE if notification should be triggered now
|
||||
checkTriggerNotification(eventName, eventData) {
|
||||
if (this.sendingNotification) {
|
||||
if (this.notificationQueue.length >= this.db.notificationSettings.maxNotificationQueue) {
|
||||
if (this.notificationQueue.length >= Database.notificationSettings.maxNotificationQueue) {
|
||||
Logger.warn(`[NotificationManager] Notification queue is full - ignoring event ${eventName}`)
|
||||
} else {
|
||||
Logger.debug(`[NotificationManager] Queueing notification ${eventName} (Queue size: ${this.notificationQueue.length})`)
|
||||
@ -92,7 +91,7 @@ class NotificationManager {
|
||||
const nextNotificationEvent = this.notificationQueue.shift()
|
||||
this.triggerNotification(nextNotificationEvent.eventName, nextNotificationEvent.eventData)
|
||||
}
|
||||
}, this.db.notificationSettings.notificationDelay)
|
||||
}, Database.notificationSettings.notificationDelay)
|
||||
}
|
||||
|
||||
sendTestNotification(notification) {
|
||||
@ -107,7 +106,7 @@ class NotificationManager {
|
||||
|
||||
sendNotification(notification, eventData) {
|
||||
const payload = notification.getApprisePayload(eventData)
|
||||
return axios.post(this.db.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
|
||||
return axios.post(Database.notificationSettings.appriseApiUrl, payload, { timeout: 6000 }).then((response) => {
|
||||
Logger.debug(`[NotificationManager] sendNotification: ${notification.eventName}/${notification.id} response=`, response.data)
|
||||
return true
|
||||
}).catch((error) => {
|
||||
|
@ -2,6 +2,7 @@ const Path = require('path')
|
||||
const serverVersion = require('../../package.json').version
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const date = require('../libs/dateAndTime')
|
||||
const fs = require('../libs/fsExtra')
|
||||
@ -15,8 +16,7 @@ const DeviceInfo = require('../objects/DeviceInfo')
|
||||
const Stream = require('../objects/Stream')
|
||||
|
||||
class PlaybackSessionManager {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
constructor() {
|
||||
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
||||
|
||||
this.sessions = []
|
||||
@ -33,19 +33,31 @@ class PlaybackSessionManager {
|
||||
return session?.stream || null
|
||||
}
|
||||
|
||||
getDeviceInfo(req) {
|
||||
async getDeviceInfo(req) {
|
||||
const ua = uaParserJs(req.headers['user-agent'])
|
||||
const ip = requestIp.getClientIp(req)
|
||||
|
||||
const clientDeviceInfo = req.body?.deviceInfo || null
|
||||
|
||||
const deviceInfo = new DeviceInfo()
|
||||
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion)
|
||||
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user.id)
|
||||
|
||||
if (clientDeviceInfo?.deviceId) {
|
||||
const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId)
|
||||
if (existingDevice) {
|
||||
if (existingDevice.update(deviceInfo)) {
|
||||
await Database.updateDevice(existingDevice)
|
||||
}
|
||||
return existingDevice
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return deviceInfo
|
||||
}
|
||||
|
||||
async startSessionRequest(req, res, episodeId) {
|
||||
const deviceInfo = this.getDeviceInfo(req)
|
||||
const deviceInfo = await this.getDeviceInfo(req)
|
||||
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
|
||||
const { user, libraryItem, body: options } = req
|
||||
const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options)
|
||||
@ -77,7 +89,7 @@ class PlaybackSessionManager {
|
||||
}
|
||||
|
||||
async syncLocalSession(user, sessionJson) {
|
||||
const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(sessionJson.libraryItemId)
|
||||
const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
||||
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
||||
Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
||||
@ -88,12 +100,12 @@ class PlaybackSessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
let session = await this.db.getPlaybackSession(sessionJson.id)
|
||||
let session = await Database.getPlaybackSession(sessionJson.id)
|
||||
if (!session) {
|
||||
// New session from local
|
||||
session = new PlaybackSession(sessionJson)
|
||||
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
|
||||
await this.db.insertEntity('session', session)
|
||||
await Database.createPlaybackSession(session)
|
||||
} else {
|
||||
session.currentTime = sessionJson.currentTime
|
||||
session.timeListening = sessionJson.timeListening
|
||||
@ -102,7 +114,7 @@ class PlaybackSessionManager {
|
||||
session.dayOfWeek = date.format(new Date(), 'dddd')
|
||||
|
||||
Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`)
|
||||
await this.db.updateEntity('session', session)
|
||||
await Database.updatePlaybackSession(session)
|
||||
}
|
||||
|
||||
const result = {
|
||||
@ -126,8 +138,8 @@ class PlaybackSessionManager {
|
||||
|
||||
// Update user and emit socket event
|
||||
if (result.progressSynced) {
|
||||
await this.db.updateEntity('user', user)
|
||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||
if (itemProgress) await Database.upsertMediaProgress(itemProgress)
|
||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||
id: itemProgress.id,
|
||||
sessionId: session.id,
|
||||
@ -155,7 +167,7 @@ class PlaybackSessionManager {
|
||||
|
||||
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
|
||||
// Close any sessions already open for user and device
|
||||
const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.deviceId)
|
||||
const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.id)
|
||||
for (const session of userSessions) {
|
||||
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`)
|
||||
await this.closeSession(user, session, null)
|
||||
@ -213,13 +225,13 @@ class PlaybackSessionManager {
|
||||
user.currentSessionId = newPlaybackSession.id
|
||||
|
||||
this.sessions.push(newPlaybackSession)
|
||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems))
|
||||
|
||||
return newPlaybackSession
|
||||
}
|
||||
|
||||
async syncSession(user, session, syncData) {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === session.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||
return null
|
||||
@ -236,9 +248,8 @@ class PlaybackSessionManager {
|
||||
}
|
||||
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
||||
if (wasUpdated) {
|
||||
|
||||
await this.db.updateEntity('user', user)
|
||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||
if (itemProgress) await Database.upsertMediaProgress(itemProgress)
|
||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||
id: itemProgress.id,
|
||||
sessionId: session.id,
|
||||
@ -259,7 +270,7 @@ class PlaybackSessionManager {
|
||||
await this.saveSession(session)
|
||||
}
|
||||
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
|
||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
||||
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, Database.libraryItems))
|
||||
SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id)
|
||||
return this.removeSession(session.id)
|
||||
}
|
||||
@ -268,10 +279,10 @@ class PlaybackSessionManager {
|
||||
if (!session.timeListening) return // Do not save a session with no listening time
|
||||
|
||||
if (session.lastSave) {
|
||||
return this.db.updateEntity('session', session)
|
||||
return Database.updatePlaybackSession(session)
|
||||
} else {
|
||||
session.lastSave = Date.now()
|
||||
return this.db.insertEntity('session', session)
|
||||
return Database.createPlaybackSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,16 +316,5 @@ class PlaybackSessionManager {
|
||||
Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Android app v0.9.54 and below had a bug where listening time was sending unix timestamp
|
||||
// See https://github.com/advplyr/audiobookshelf/issues/868
|
||||
// Remove playback sessions with listening time too high
|
||||
async removeInvalidSessions() {
|
||||
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000
|
||||
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
||||
if (numSessionsRemoved) {
|
||||
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = PlaybackSessionManager
|
||||
|
@ -1,5 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
@ -19,8 +20,7 @@ const AudioFile = require('../objects/files/AudioFile')
|
||||
const Task = require("../objects/Task")
|
||||
|
||||
class PodcastManager {
|
||||
constructor(db, watcher, notificationManager, taskManager) {
|
||||
this.db = db
|
||||
constructor(watcher, notificationManager, taskManager) {
|
||||
this.watcher = watcher
|
||||
this.notificationManager = notificationManager
|
||||
this.taskManager = taskManager
|
||||
@ -32,10 +32,6 @@ class PodcastManager {
|
||||
this.MaxFailedEpisodeChecks = 24
|
||||
}
|
||||
|
||||
get serverSettings() {
|
||||
return this.db.serverSettings || {}
|
||||
}
|
||||
|
||||
getEpisodeDownloadsInQueue(libraryItemId) {
|
||||
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
||||
}
|
||||
@ -59,6 +55,7 @@ class PodcastManager {
|
||||
const newPe = new PodcastEpisode()
|
||||
newPe.setData(ep, index++)
|
||||
newPe.libraryItemId = libraryItem.id
|
||||
newPe.podcastId = libraryItem.media.id
|
||||
const newPeDl = new PodcastEpisodeDownload()
|
||||
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
||||
this.startPodcastEpisodeDownload(newPeDl)
|
||||
@ -153,7 +150,7 @@ class PodcastManager {
|
||||
return false
|
||||
}
|
||||
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
||||
return false
|
||||
@ -182,7 +179,7 @@ class PodcastManager {
|
||||
}
|
||||
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded()
|
||||
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
|
||||
@ -235,6 +232,7 @@ class PodcastManager {
|
||||
}
|
||||
const newAudioFile = new AudioFile()
|
||||
newAudioFile.setDataFromProbe(libraryFile, mediaProbeData)
|
||||
newAudioFile.index = 1
|
||||
return newAudioFile
|
||||
}
|
||||
|
||||
@ -274,7 +272,7 @@ class PodcastManager {
|
||||
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
return libraryItem.media.autoDownloadEpisodes
|
||||
}
|
||||
@ -313,7 +311,7 @@ class PodcastManager {
|
||||
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
||||
return newEpisodes
|
||||
|
@ -2,14 +2,13 @@ const Path = require('path')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Feed = require('../objects/Feed')
|
||||
|
||||
class RssFeedManager {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
|
||||
constructor() {
|
||||
this.feeds = {}
|
||||
}
|
||||
|
||||
@ -19,18 +18,18 @@ class RssFeedManager {
|
||||
|
||||
validateFeedEntity(feedObj) {
|
||||
if (feedObj.entityType === 'collection') {
|
||||
if (!this.db.collections.some(li => li.id === feedObj.entityId)) {
|
||||
if (!Database.collections.some(li => li.id === feedObj.entityId)) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
|
||||
return false
|
||||
}
|
||||
} else if (feedObj.entityType === 'libraryItem') {
|
||||
if (!this.db.libraryItems.some(li => li.id === feedObj.entityId)) {
|
||||
if (!Database.libraryItems.some(li => li.id === feedObj.entityId)) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
|
||||
return false
|
||||
}
|
||||
} else if (feedObj.entityType === 'series') {
|
||||
const series = this.db.series.find(s => s.id === feedObj.entityId)
|
||||
const hasSeriesBook = this.db.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
const series = Database.series.find(s => s.id === feedObj.entityId)
|
||||
const hasSeriesBook = Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
if (!hasSeriesBook) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`)
|
||||
return false
|
||||
@ -43,19 +42,13 @@ class RssFeedManager {
|
||||
}
|
||||
|
||||
async init() {
|
||||
const feedObjects = await this.db.getAllEntities('feed')
|
||||
if (!feedObjects || !feedObjects.length) return
|
||||
const feedObjects = Database.feeds
|
||||
if (!feedObjects?.length) return
|
||||
|
||||
for (const feedObj of feedObjects) {
|
||||
// Migration: In v2.2.12 entityType "item" was updated to "libraryItem"
|
||||
if (feedObj.entityType === 'item') {
|
||||
feedObj.entityType = 'libraryItem'
|
||||
await this.db.updateEntity('feed', feedObj)
|
||||
}
|
||||
|
||||
// Remove invalid feeds
|
||||
if (!this.validateFeedEntity(feedObj)) {
|
||||
await this.db.removeEntity('feed', feedObj.id)
|
||||
await Database.removeFeed(feedObj.id)
|
||||
}
|
||||
|
||||
const feed = new Feed(feedObj)
|
||||
@ -82,7 +75,7 @@ class RssFeedManager {
|
||||
|
||||
// Check if feed needs to be updated
|
||||
if (feed.entityType === 'libraryItem') {
|
||||
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
||||
const libraryItem = Database.getLibraryItem(feed.entityId)
|
||||
|
||||
let mostRecentlyUpdatedAt = libraryItem.updatedAt
|
||||
if (libraryItem.isPodcast) {
|
||||
@ -94,12 +87,12 @@ class RssFeedManager {
|
||||
if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
||||
feed.updateFromItem(libraryItem)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
await Database.updateFeed(feed)
|
||||
}
|
||||
} else if (feed.entityType === 'collection') {
|
||||
const collection = this.db.collections.find(c => c.id === feed.entityId)
|
||||
const collection = Database.collections.find(c => c.id === feed.entityId)
|
||||
if (collection) {
|
||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Find most recently updated item in collection
|
||||
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
||||
@ -113,15 +106,15 @@ class RssFeedManager {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
|
||||
|
||||
feed.updateFromCollection(collectionExpanded)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
await Database.updateFeed(feed)
|
||||
}
|
||||
}
|
||||
} else if (feed.entityType === 'series') {
|
||||
const series = this.db.series.find(s => s.id === feed.entityId)
|
||||
const series = Database.series.find(s => s.id === feed.entityId)
|
||||
if (series) {
|
||||
const seriesJson = series.toJSON()
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
|
||||
// Find most recently updated item in series
|
||||
let mostRecentlyUpdatedAt = seriesJson.updatedAt
|
||||
@ -140,7 +133,7 @@ class RssFeedManager {
|
||||
Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`)
|
||||
|
||||
feed.updateFromSeries(seriesJson)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
await Database.updateFeed(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,7 +190,7 @@ class RssFeedManager {
|
||||
this.feeds[feed.id] = feed
|
||||
|
||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||
await this.db.insertEntity('feed', feed)
|
||||
await Database.createFeed(feed)
|
||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||
return feed
|
||||
}
|
||||
@ -214,7 +207,7 @@ class RssFeedManager {
|
||||
this.feeds[feed.id] = feed
|
||||
|
||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||
await this.db.insertEntity('feed', feed)
|
||||
await Database.createFeed(feed)
|
||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||
return feed
|
||||
}
|
||||
@ -231,14 +224,14 @@ class RssFeedManager {
|
||||
this.feeds[feed.id] = feed
|
||||
|
||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||
await this.db.insertEntity('feed', feed)
|
||||
await Database.createFeed(feed)
|
||||
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
||||
return feed
|
||||
}
|
||||
|
||||
async handleCloseFeed(feed) {
|
||||
if (!feed) return
|
||||
await this.db.removeEntity('feed', feed.id)
|
||||
await Database.removeFeed(feed.id)
|
||||
SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified())
|
||||
delete this.feeds[feed.id]
|
||||
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
||||
|
78
server/models/Author.js
Normal file
78
server/models/Author.js
Normal file
@ -0,0 +1,78 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldAuthor = require('../objects/entities/Author')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Author extends Model {
|
||||
static async getOldAuthors() {
|
||||
const authors = await this.findAll()
|
||||
return authors.map(au => au.getOldAuthor())
|
||||
}
|
||||
|
||||
getOldAuthor() {
|
||||
return new oldAuthor({
|
||||
id: this.id,
|
||||
asin: this.asin,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
imagePath: this.imagePath,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static updateFromOld(oldAuthor) {
|
||||
const author = this.getFromOld(oldAuthor)
|
||||
return this.update(author, {
|
||||
where: {
|
||||
id: author.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldAuthor) {
|
||||
const author = this.getFromOld(oldAuthor)
|
||||
return this.create(author)
|
||||
}
|
||||
|
||||
static createBulkFromOld(oldAuthors) {
|
||||
const authors = oldAuthors.map(this.getFromOld)
|
||||
return this.bulkCreate(authors)
|
||||
}
|
||||
|
||||
static getFromOld(oldAuthor) {
|
||||
return {
|
||||
id: oldAuthor.id,
|
||||
name: oldAuthor.name,
|
||||
asin: oldAuthor.asin,
|
||||
description: oldAuthor.description,
|
||||
imagePath: oldAuthor.imagePath
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(authorId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: authorId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Author.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
imagePath: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'author'
|
||||
})
|
||||
|
||||
return Author
|
||||
}
|
121
server/models/Book.js
Normal file
121
server/models/Book.js
Normal file
@ -0,0 +1,121 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Book extends Model {
|
||||
static getOldBook(libraryItemExpanded) {
|
||||
const bookExpanded = libraryItemExpanded.media
|
||||
const authors = bookExpanded.authors.map(au => {
|
||||
return {
|
||||
id: au.id,
|
||||
name: au.name
|
||||
}
|
||||
})
|
||||
const series = bookExpanded.series.map(se => {
|
||||
return {
|
||||
id: se.id,
|
||||
name: se.name,
|
||||
sequence: se.bookSeries.sequence
|
||||
}
|
||||
})
|
||||
return {
|
||||
id: bookExpanded.id,
|
||||
libraryItemId: libraryItemExpanded.id,
|
||||
coverPath: bookExpanded.coverPath,
|
||||
tags: bookExpanded.tags,
|
||||
audioFiles: bookExpanded.audioFiles,
|
||||
chapters: bookExpanded.chapters,
|
||||
ebookFile: bookExpanded.ebookFile,
|
||||
metadata: {
|
||||
title: bookExpanded.title,
|
||||
subtitle: bookExpanded.subtitle,
|
||||
authors: authors,
|
||||
narrators: bookExpanded.narrators,
|
||||
series: series,
|
||||
genres: bookExpanded.genres,
|
||||
publishedYear: bookExpanded.publishedYear,
|
||||
publishedDate: bookExpanded.publishedDate,
|
||||
publisher: bookExpanded.publisher,
|
||||
description: bookExpanded.description,
|
||||
isbn: bookExpanded.isbn,
|
||||
asin: bookExpanded.asin,
|
||||
language: bookExpanded.language,
|
||||
explicit: bookExpanded.explicit,
|
||||
abridged: bookExpanded.abridged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} oldBook
|
||||
* @returns {boolean} true if updated
|
||||
*/
|
||||
static saveFromOld(oldBook) {
|
||||
const book = this.getFromOld(oldBook)
|
||||
return this.update(book, {
|
||||
where: {
|
||||
id: book.id
|
||||
}
|
||||
}).then(result => result[0] > 0).catch((error) => {
|
||||
Logger.error(`[Book] Failed to save book ${book.id}`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldBook) {
|
||||
return {
|
||||
id: oldBook.id,
|
||||
title: oldBook.metadata.title,
|
||||
subtitle: oldBook.metadata.subtitle,
|
||||
publishedYear: oldBook.metadata.publishedYear,
|
||||
publishedDate: oldBook.metadata.publishedDate,
|
||||
publisher: oldBook.metadata.publisher,
|
||||
description: oldBook.metadata.description,
|
||||
isbn: oldBook.metadata.isbn,
|
||||
asin: oldBook.metadata.asin,
|
||||
language: oldBook.metadata.language,
|
||||
explicit: !!oldBook.metadata.explicit,
|
||||
abridged: !!oldBook.metadata.abridged,
|
||||
narrators: oldBook.metadata.narrators,
|
||||
ebookFile: oldBook.ebookFile?.toJSON() || null,
|
||||
coverPath: oldBook.coverPath,
|
||||
audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [],
|
||||
chapters: oldBook.chapters,
|
||||
tags: oldBook.tags,
|
||||
genres: oldBook.metadata.genres
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Book.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING,
|
||||
publishedYear: DataTypes.STRING,
|
||||
publishedDate: DataTypes.STRING,
|
||||
publisher: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
isbn: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
abridged: DataTypes.BOOLEAN,
|
||||
coverPath: DataTypes.STRING,
|
||||
|
||||
narrators: DataTypes.JSON,
|
||||
audioFiles: DataTypes.JSON,
|
||||
ebookFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'book'
|
||||
})
|
||||
|
||||
return Book
|
||||
}
|
40
server/models/BookAuthor.js
Normal file
40
server/models/BookAuthor.js
Normal file
@ -0,0 +1,40 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class BookAuthor extends Model {
|
||||
static removeByIds(authorId = null, bookId = null) {
|
||||
const where = {}
|
||||
if (authorId) where.authorId = authorId
|
||||
if (bookId) where.bookId = bookId
|
||||
return this.destroy({
|
||||
where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
BookAuthor.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'bookAuthor',
|
||||
timestamps: false
|
||||
})
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
const { book, author } = sequelize.models
|
||||
book.belongsToMany(author, { through: BookAuthor })
|
||||
author.belongsToMany(book, { through: BookAuthor })
|
||||
|
||||
book.hasMany(BookAuthor)
|
||||
BookAuthor.belongsTo(book)
|
||||
|
||||
author.hasMany(BookAuthor)
|
||||
BookAuthor.belongsTo(author)
|
||||
|
||||
return BookAuthor
|
||||
}
|
41
server/models/BookSeries.js
Normal file
41
server/models/BookSeries.js
Normal file
@ -0,0 +1,41 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class BookSeries extends Model {
|
||||
static removeByIds(seriesId = null, bookId = null) {
|
||||
const where = {}
|
||||
if (seriesId) where.seriesId = seriesId
|
||||
if (bookId) where.bookId = bookId
|
||||
return this.destroy({
|
||||
where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
BookSeries.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
sequence: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'bookSeries',
|
||||
timestamps: false
|
||||
})
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
const { book, series } = sequelize.models
|
||||
book.belongsToMany(series, { through: BookSeries })
|
||||
series.belongsToMany(book, { through: BookSeries })
|
||||
|
||||
book.hasMany(BookSeries)
|
||||
BookSeries.belongsTo(book)
|
||||
|
||||
series.hasMany(BookSeries)
|
||||
BookSeries.belongsTo(series)
|
||||
|
||||
return BookSeries
|
||||
}
|
117
server/models/Collection.js
Normal file
117
server/models/Collection.js
Normal file
@ -0,0 +1,117 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldCollection = require('../objects/Collection')
|
||||
const { areEquivalent } = require('../utils/index')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Collection extends Model {
|
||||
static async getOldCollections() {
|
||||
const collections = await this.findAll({
|
||||
include: {
|
||||
model: sequelize.models.book,
|
||||
include: sequelize.models.libraryItem
|
||||
}
|
||||
})
|
||||
return collections.map(c => this.getOldCollection(c))
|
||||
}
|
||||
|
||||
static getOldCollection(collectionExpanded) {
|
||||
const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || []
|
||||
return new oldCollection({
|
||||
id: collectionExpanded.id,
|
||||
libraryId: collectionExpanded.libraryId,
|
||||
name: collectionExpanded.name,
|
||||
description: collectionExpanded.description,
|
||||
books: libraryItemIds,
|
||||
lastUpdate: collectionExpanded.updatedAt.valueOf(),
|
||||
createdAt: collectionExpanded.createdAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldCollection) {
|
||||
const collection = this.getFromOld(oldCollection)
|
||||
return this.create(collection)
|
||||
}
|
||||
|
||||
static async fullUpdateFromOld(oldCollection, collectionBooks) {
|
||||
const existingCollection = await this.findByPk(oldCollection.id, {
|
||||
include: sequelize.models.collectionBook
|
||||
})
|
||||
if (!existingCollection) return false
|
||||
|
||||
let hasUpdates = false
|
||||
const collection = this.getFromOld(oldCollection)
|
||||
|
||||
for (const cb of collectionBooks) {
|
||||
const existingCb = existingCollection.collectionBooks.find(i => i.bookId === cb.bookId)
|
||||
if (!existingCb) {
|
||||
await sequelize.models.collectionBook.create(cb)
|
||||
hasUpdates = true
|
||||
} else if (existingCb.order != cb.order) {
|
||||
await existingCb.update({ order: cb.order })
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const cb of existingCollection.collectionBooks) {
|
||||
// collectionBook was removed
|
||||
if (!collectionBooks.some(i => i.bookId === cb.bookId)) {
|
||||
await cb.destroy()
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
let hasCollectionUpdates = false
|
||||
for (const key in collection) {
|
||||
let existingValue = existingCollection[key]
|
||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||
if (!areEquivalent(collection[key], existingValue)) {
|
||||
hasCollectionUpdates = true
|
||||
}
|
||||
}
|
||||
if (hasCollectionUpdates) {
|
||||
existingCollection.update(collection)
|
||||
hasUpdates = true
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
static getFromOld(oldCollection) {
|
||||
return {
|
||||
id: oldCollection.id,
|
||||
name: oldCollection.name,
|
||||
description: oldCollection.description,
|
||||
createdAt: oldCollection.createdAt,
|
||||
updatedAt: oldCollection.lastUpdate,
|
||||
libraryId: oldCollection.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(collectionId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: collectionId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Collection.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'collection'
|
||||
})
|
||||
|
||||
const { library } = sequelize.models
|
||||
|
||||
library.hasMany(Collection)
|
||||
Collection.belongsTo(library)
|
||||
|
||||
return Collection
|
||||
}
|
42
server/models/CollectionBook.js
Normal file
42
server/models/CollectionBook.js
Normal file
@ -0,0 +1,42 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class CollectionBook extends Model {
|
||||
static removeByIds(collectionId, bookId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
bookId,
|
||||
collectionId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
CollectionBook.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
order: DataTypes.INTEGER
|
||||
}, {
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'collectionBook'
|
||||
})
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
const { book, collection } = sequelize.models
|
||||
book.belongsToMany(collection, { through: CollectionBook })
|
||||
collection.belongsToMany(book, { through: CollectionBook })
|
||||
|
||||
book.hasMany(CollectionBook)
|
||||
CollectionBook.belongsTo(book)
|
||||
|
||||
collection.hasMany(CollectionBook)
|
||||
CollectionBook.belongsTo(collection)
|
||||
|
||||
return CollectionBook
|
||||
}
|
112
server/models/Device.js
Normal file
112
server/models/Device.js
Normal file
@ -0,0 +1,112 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const oldDevice = require('../objects/DeviceInfo')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Device extends Model {
|
||||
getOldDevice() {
|
||||
let browserVersion = null
|
||||
let sdkVersion = null
|
||||
if (this.clientName === 'Abs Android') {
|
||||
sdkVersion = this.deviceVersion || null
|
||||
} else {
|
||||
browserVersion = this.deviceVersion || null
|
||||
}
|
||||
|
||||
return new oldDevice({
|
||||
id: this.id,
|
||||
deviceId: this.deviceId,
|
||||
userId: this.userId,
|
||||
ipAddress: this.ipAddress,
|
||||
browserName: this.extraData.browserName || null,
|
||||
browserVersion,
|
||||
osName: this.extraData.osName || null,
|
||||
osVersion: this.extraData.osVersion || null,
|
||||
clientVersion: this.clientVersion || null,
|
||||
manufacturer: this.extraData.manufacturer || null,
|
||||
model: this.extraData.model || null,
|
||||
sdkVersion,
|
||||
deviceName: this.deviceName,
|
||||
clientName: this.clientName
|
||||
})
|
||||
}
|
||||
|
||||
static async getOldDeviceByDeviceId(deviceId) {
|
||||
const device = await this.findOne({
|
||||
deviceId
|
||||
})
|
||||
if (!device) return null
|
||||
return device.getOldDevice()
|
||||
}
|
||||
|
||||
static createFromOld(oldDevice) {
|
||||
const device = this.getFromOld(oldDevice)
|
||||
return this.create(device)
|
||||
}
|
||||
|
||||
static updateFromOld(oldDevice) {
|
||||
const device = this.getFromOld(oldDevice)
|
||||
return this.update(device, {
|
||||
where: {
|
||||
id: device.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldDeviceInfo) {
|
||||
let extraData = {}
|
||||
|
||||
if (oldDeviceInfo.manufacturer) {
|
||||
extraData.manufacturer = oldDeviceInfo.manufacturer
|
||||
}
|
||||
if (oldDeviceInfo.model) {
|
||||
extraData.model = oldDeviceInfo.model
|
||||
}
|
||||
if (oldDeviceInfo.osName) {
|
||||
extraData.osName = oldDeviceInfo.osName
|
||||
}
|
||||
if (oldDeviceInfo.osVersion) {
|
||||
extraData.osVersion = oldDeviceInfo.osVersion
|
||||
}
|
||||
if (oldDeviceInfo.browserName) {
|
||||
extraData.browserName = oldDeviceInfo.browserName
|
||||
}
|
||||
|
||||
return {
|
||||
id: oldDeviceInfo.id,
|
||||
deviceId: oldDeviceInfo.deviceId,
|
||||
clientName: oldDeviceInfo.clientName || null,
|
||||
clientVersion: oldDeviceInfo.clientVersion || null,
|
||||
ipAddress: oldDeviceInfo.ipAddress,
|
||||
deviceName: oldDeviceInfo.deviceName || null,
|
||||
deviceVersion: oldDeviceInfo.sdkVersion || oldDeviceInfo.browserVersion || null,
|
||||
userId: oldDeviceInfo.userId,
|
||||
extraData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Device.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
deviceId: DataTypes.STRING,
|
||||
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
|
||||
clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
|
||||
ipAddress: DataTypes.STRING,
|
||||
deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||
deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'device'
|
||||
})
|
||||
|
||||
const { user } = sequelize.models
|
||||
|
||||
user.hasMany(Device)
|
||||
Device.belongsTo(user)
|
||||
|
||||
return Device
|
||||
}
|
165
server/models/Feed.js
Normal file
165
server/models/Feed.js
Normal file
@ -0,0 +1,165 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const oldFeed = require('../objects/Feed')
|
||||
/*
|
||||
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||
* Feeds can be created from LibraryItem, Collection, Playlist or Series
|
||||
*/
|
||||
module.exports = (sequelize) => {
|
||||
class Feed extends Model {
|
||||
static async getOldFeeds() {
|
||||
const feeds = await this.findAll({
|
||||
include: {
|
||||
model: sequelize.models.feedEpisode
|
||||
}
|
||||
})
|
||||
return feeds.map(f => this.getOldFeed(f))
|
||||
}
|
||||
|
||||
static getOldFeed(feedExpanded) {
|
||||
const episodes = feedExpanded.feedEpisodes.map((feedEpisode) => feedEpisode.getOldEpisode())
|
||||
|
||||
return new oldFeed({
|
||||
id: feedExpanded.id,
|
||||
slug: feedExpanded.slug,
|
||||
userId: feedExpanded.userId,
|
||||
entityType: feedExpanded.entityType,
|
||||
entityId: feedExpanded.entityId,
|
||||
meta: {
|
||||
title: feedExpanded.title,
|
||||
description: feedExpanded.description,
|
||||
author: feedExpanded.author,
|
||||
imageUrl: feedExpanded.imageURL,
|
||||
feedUrl: feedExpanded.feedURL,
|
||||
link: feedExpanded.siteURL,
|
||||
explicit: feedExpanded.explicit,
|
||||
type: feedExpanded.podcastType,
|
||||
language: feedExpanded.language,
|
||||
preventIndexing: feedExpanded.preventIndexing,
|
||||
ownerName: feedExpanded.ownerName,
|
||||
ownerEmail: feedExpanded.ownerEmail
|
||||
},
|
||||
serverAddress: feedExpanded.serverAddress,
|
||||
feedUrl: feedExpanded.feedURL,
|
||||
episodes,
|
||||
createdAt: feedExpanded.createdAt.valueOf(),
|
||||
updatedAt: feedExpanded.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static removeById(feedId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: feedId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getEntity(options) {
|
||||
if (!this.entityType) return Promise.resolve(null)
|
||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}`
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
}
|
||||
|
||||
Feed.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
slug: DataTypes.STRING,
|
||||
entityType: DataTypes.STRING,
|
||||
entityId: DataTypes.UUIDV4,
|
||||
entityUpdatedAt: DataTypes.DATE,
|
||||
serverAddress: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
siteURL: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
author: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
ownerName: DataTypes.STRING,
|
||||
ownerEmail: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
preventIndexing: DataTypes.BOOLEAN
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'feed'
|
||||
})
|
||||
|
||||
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
||||
|
||||
user.hasMany(Feed)
|
||||
Feed.belongsTo(user)
|
||||
|
||||
libraryItem.hasMany(Feed, {
|
||||
foreignKey: 'entityId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
entityType: 'libraryItem'
|
||||
}
|
||||
})
|
||||
Feed.belongsTo(libraryItem, { foreignKey: 'entityId', constraints: false })
|
||||
|
||||
collection.hasMany(Feed, {
|
||||
foreignKey: 'entityId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
entityType: 'collection'
|
||||
}
|
||||
})
|
||||
Feed.belongsTo(collection, { foreignKey: 'entityId', constraints: false })
|
||||
|
||||
series.hasMany(Feed, {
|
||||
foreignKey: 'entityId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
entityType: 'series'
|
||||
}
|
||||
})
|
||||
Feed.belongsTo(series, { foreignKey: 'entityId', constraints: false })
|
||||
|
||||
playlist.hasMany(Feed, {
|
||||
foreignKey: 'entityId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
entityType: 'playlist'
|
||||
}
|
||||
})
|
||||
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
||||
|
||||
Feed.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
for (const instance of findResult) {
|
||||
if (instance.entityType === 'libraryItem' && instance.libraryItem !== undefined) {
|
||||
instance.entity = instance.libraryItem
|
||||
instance.dataValues.entity = instance.dataValues.libraryItem
|
||||
} else if (instance.entityType === 'collection' && instance.collection !== undefined) {
|
||||
instance.entity = instance.collection
|
||||
instance.dataValues.entity = instance.dataValues.collection
|
||||
} else if (instance.entityType === 'series' && instance.series !== undefined) {
|
||||
instance.entity = instance.series
|
||||
instance.dataValues.entity = instance.dataValues.series
|
||||
} else if (instance.entityType === 'playlist' && instance.playlist !== undefined) {
|
||||
instance.entity = instance.playlist
|
||||
instance.dataValues.entity = instance.dataValues.playlist
|
||||
}
|
||||
|
||||
// To prevent mistakes:
|
||||
delete instance.libraryItem
|
||||
delete instance.dataValues.libraryItem
|
||||
delete instance.collection
|
||||
delete instance.dataValues.collection
|
||||
delete instance.series
|
||||
delete instance.dataValues.series
|
||||
delete instance.playlist
|
||||
delete instance.dataValues.playlist
|
||||
}
|
||||
})
|
||||
|
||||
return Feed
|
||||
}
|
60
server/models/FeedEpisode.js
Normal file
60
server/models/FeedEpisode.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class FeedEpisode extends Model {
|
||||
getOldEpisode() {
|
||||
const enclosure = {
|
||||
url: this.enclosureURL,
|
||||
size: this.enclosureSize,
|
||||
type: this.enclosureType
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
enclosure,
|
||||
pubDate: this.pubDate,
|
||||
link: this.siteURL,
|
||||
author: this.author,
|
||||
explicit: this.explicit,
|
||||
duration: this.duration,
|
||||
season: this.season,
|
||||
episode: this.episode,
|
||||
episodeType: this.episodeType,
|
||||
fullPath: this.filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FeedEpisode.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
siteURL: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureType: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
pubDate: DataTypes.STRING,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
filePath: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'feedEpisode'
|
||||
})
|
||||
|
||||
const { feed } = sequelize.models
|
||||
|
||||
feed.hasMany(FeedEpisode)
|
||||
FeedEpisode.belongsTo(feed)
|
||||
|
||||
return FeedEpisode
|
||||
}
|
137
server/models/Library.js
Normal file
137
server/models/Library.js
Normal file
@ -0,0 +1,137 @@
|
||||
const Logger = require('../Logger')
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldLibrary = require('../objects/Library')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Library extends Model {
|
||||
static async getAllOldLibraries() {
|
||||
const libraries = await this.findAll({
|
||||
include: sequelize.models.libraryFolder
|
||||
})
|
||||
return libraries.map(lib => this.getOldLibrary(lib))
|
||||
}
|
||||
|
||||
static getOldLibrary(libraryExpanded) {
|
||||
const folders = libraryExpanded.libraryFolders.map(folder => {
|
||||
return {
|
||||
id: folder.id,
|
||||
fullPath: folder.path,
|
||||
libraryId: folder.libraryId,
|
||||
addedAt: folder.createdAt.valueOf()
|
||||
}
|
||||
})
|
||||
return new oldLibrary({
|
||||
id: libraryExpanded.id,
|
||||
name: libraryExpanded.name,
|
||||
folders,
|
||||
displayOrder: libraryExpanded.displayOrder,
|
||||
icon: libraryExpanded.icon,
|
||||
mediaType: libraryExpanded.mediaType,
|
||||
provider: libraryExpanded.provider,
|
||||
settings: libraryExpanded.settings,
|
||||
createdAt: libraryExpanded.createdAt.valueOf(),
|
||||
lastUpdate: libraryExpanded.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} oldLibrary
|
||||
* @returns {Library|null}
|
||||
*/
|
||||
static createFromOld(oldLibrary) {
|
||||
const library = this.getFromOld(oldLibrary)
|
||||
|
||||
library.libraryFolders = oldLibrary.folders.map(folder => {
|
||||
return {
|
||||
id: folder.id,
|
||||
path: folder.fullPath,
|
||||
libraryId: library.id
|
||||
}
|
||||
})
|
||||
|
||||
return this.create(library).catch((error) => {
|
||||
Logger.error(`[Library] Failed to create library ${library.id}`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
static async updateFromOld(oldLibrary) {
|
||||
const existingLibrary = await this.findByPk(oldLibrary.id, {
|
||||
include: sequelize.models.libraryFolder
|
||||
})
|
||||
if (!existingLibrary) {
|
||||
Logger.error(`[Library] Failed to update library ${oldLibrary.id} - not found`)
|
||||
return null
|
||||
}
|
||||
|
||||
const library = this.getFromOld(oldLibrary)
|
||||
|
||||
const libraryFolders = oldLibrary.folders.map(folder => {
|
||||
return {
|
||||
id: folder.id,
|
||||
path: folder.fullPath,
|
||||
libraryId: library.id
|
||||
}
|
||||
})
|
||||
for (const libraryFolder of libraryFolders) {
|
||||
const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id)
|
||||
if (!existingLibraryFolder) {
|
||||
await sequelize.models.libraryFolder.create(libraryFolder)
|
||||
} else if (existingLibraryFolder.path !== libraryFolder.path) {
|
||||
await existingLibraryFolder.update({ path: libraryFolder.path })
|
||||
}
|
||||
}
|
||||
|
||||
const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id))
|
||||
for (const existingLibraryFolder of libraryFoldersRemoved) {
|
||||
await existingLibraryFolder.destroy()
|
||||
}
|
||||
|
||||
return existingLibrary.update(library)
|
||||
}
|
||||
|
||||
static getFromOld(oldLibrary) {
|
||||
return {
|
||||
id: oldLibrary.id,
|
||||
name: oldLibrary.name,
|
||||
displayOrder: oldLibrary.displayOrder,
|
||||
icon: oldLibrary.icon || null,
|
||||
mediaType: oldLibrary.mediaType || null,
|
||||
provider: oldLibrary.provider,
|
||||
settings: oldLibrary.settings?.toJSON() || {},
|
||||
createdAt: oldLibrary.createdAt,
|
||||
updatedAt: oldLibrary.lastUpdate
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(libraryId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: libraryId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Library.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
displayOrder: DataTypes.INTEGER,
|
||||
icon: DataTypes.STRING,
|
||||
mediaType: DataTypes.STRING,
|
||||
provider: DataTypes.STRING,
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
settings: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'library'
|
||||
})
|
||||
|
||||
return Library
|
||||
}
|
23
server/models/LibraryFolder.js
Normal file
23
server/models/LibraryFolder.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class LibraryFolder extends Model { }
|
||||
|
||||
LibraryFolder.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
path: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'libraryFolder'
|
||||
})
|
||||
|
||||
const { library } = sequelize.models
|
||||
library.hasMany(LibraryFolder)
|
||||
LibraryFolder.belongsTo(library)
|
||||
|
||||
return LibraryFolder
|
||||
}
|
393
server/models/LibraryItem.js
Normal file
393
server/models/LibraryItem.js
Normal file
@ -0,0 +1,393 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const oldLibraryItem = require('../objects/LibraryItem')
|
||||
const { areEquivalent } = require('../utils/index')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class LibraryItem extends Model {
|
||||
static async getAllOldLibraryItems() {
|
||||
let libraryItems = await this.findAll({
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.book,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: sequelize.models.podcast,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.podcastEpisode
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
return libraryItems.map(ti => this.getOldLibraryItem(ti))
|
||||
}
|
||||
|
||||
static getOldLibraryItem(libraryItemExpanded) {
|
||||
let media = null
|
||||
if (libraryItemExpanded.mediaType === 'book') {
|
||||
media = sequelize.models.book.getOldBook(libraryItemExpanded)
|
||||
} else if (libraryItemExpanded.mediaType === 'podcast') {
|
||||
media = sequelize.models.podcast.getOldPodcast(libraryItemExpanded)
|
||||
}
|
||||
|
||||
return new oldLibraryItem({
|
||||
id: libraryItemExpanded.id,
|
||||
ino: libraryItemExpanded.ino,
|
||||
libraryId: libraryItemExpanded.libraryId,
|
||||
folderId: libraryItemExpanded.libraryFolderId,
|
||||
path: libraryItemExpanded.path,
|
||||
relPath: libraryItemExpanded.relPath,
|
||||
isFile: libraryItemExpanded.isFile,
|
||||
mtimeMs: libraryItemExpanded.mtime?.valueOf(),
|
||||
ctimeMs: libraryItemExpanded.ctime?.valueOf(),
|
||||
birthtimeMs: libraryItemExpanded.birthtime?.valueOf(),
|
||||
addedAt: libraryItemExpanded.createdAt.valueOf(),
|
||||
updatedAt: libraryItemExpanded.updatedAt.valueOf(),
|
||||
lastScan: libraryItemExpanded.lastScan?.valueOf(),
|
||||
scanVersion: libraryItemExpanded.lastScanVersion,
|
||||
isMissing: !!libraryItemExpanded.isMissing,
|
||||
isInvalid: !!libraryItemExpanded.isInvalid,
|
||||
mediaType: libraryItemExpanded.mediaType,
|
||||
media,
|
||||
libraryFiles: libraryItemExpanded.libraryFiles
|
||||
})
|
||||
}
|
||||
|
||||
static async fullCreateFromOld(oldLibraryItem) {
|
||||
const newLibraryItem = await this.create(this.getFromOld(oldLibraryItem))
|
||||
|
||||
if (oldLibraryItem.mediaType === 'book') {
|
||||
const bookObj = sequelize.models.book.getFromOld(oldLibraryItem.media)
|
||||
bookObj.libraryItemId = newLibraryItem.id
|
||||
const newBook = await sequelize.models.book.create(bookObj)
|
||||
|
||||
const oldBookAuthors = oldLibraryItem.media.metadata.authors || []
|
||||
const oldBookSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||
|
||||
for (const oldBookAuthor of oldBookAuthors) {
|
||||
await sequelize.models.bookAuthor.create({ authorId: oldBookAuthor.id, bookId: newBook.id })
|
||||
}
|
||||
for (const oldSeries of oldBookSeriesAll) {
|
||||
await sequelize.models.bookSeries.create({ seriesId: oldSeries.id, bookId: newBook.id, sequence: oldSeries.sequence })
|
||||
}
|
||||
} else if (oldLibraryItem.mediaType === 'podcast') {
|
||||
const podcastObj = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
|
||||
podcastObj.libraryItemId = newLibraryItem.id
|
||||
const newPodcast = await sequelize.models.podcast.create(podcastObj)
|
||||
|
||||
const oldEpisodes = oldLibraryItem.media.episodes || []
|
||||
for (const oldEpisode of oldEpisodes) {
|
||||
const episodeObj = sequelize.models.podcastEpisode.getFromOld(oldEpisode)
|
||||
episodeObj.libraryItemId = newLibraryItem.id
|
||||
episodeObj.podcastId = newPodcast.id
|
||||
await sequelize.models.podcastEpisode.create(episodeObj)
|
||||
}
|
||||
}
|
||||
|
||||
return newLibraryItem
|
||||
}
|
||||
|
||||
static async fullUpdateFromOld(oldLibraryItem) {
|
||||
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.book,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: sequelize.models.podcast,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.podcastEpisode
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
if (!libraryItemExpanded) return false
|
||||
|
||||
let hasUpdates = false
|
||||
|
||||
// Check update Book/Podcast
|
||||
if (libraryItemExpanded.media) {
|
||||
let updatedMedia = null
|
||||
if (libraryItemExpanded.mediaType === 'podcast') {
|
||||
updatedMedia = sequelize.models.podcast.getFromOld(oldLibraryItem.media)
|
||||
|
||||
const existingPodcastEpisodes = libraryItemExpanded.media.podcastEpisodes || []
|
||||
const updatedPodcastEpisodes = oldLibraryItem.media.episodes || []
|
||||
|
||||
for (const existingPodcastEpisode of existingPodcastEpisodes) {
|
||||
// Episode was removed
|
||||
if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`)
|
||||
await existingPodcastEpisode.destroy()
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const updatedPodcastEpisode of updatedPodcastEpisodes) {
|
||||
const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
|
||||
if (!existingEpisodeMatch) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
|
||||
await sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
|
||||
hasUpdates = true
|
||||
} else {
|
||||
const updatedEpisodeCleaned = sequelize.models.podcastEpisode.getFromOld(updatedPodcastEpisode)
|
||||
let episodeHasUpdates = false
|
||||
for (const key in updatedEpisodeCleaned) {
|
||||
let existingValue = existingEpisodeMatch[key]
|
||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||
|
||||
if (!areEquivalent(updatedEpisodeCleaned[key], existingValue, true)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingEpisodeMatch.title}" ${key} was updated from "${existingValue}" to "${updatedEpisodeCleaned[key]}"`)
|
||||
episodeHasUpdates = true
|
||||
}
|
||||
}
|
||||
if (episodeHasUpdates) {
|
||||
await existingEpisodeMatch.update(updatedEpisodeCleaned)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (libraryItemExpanded.mediaType === 'book') {
|
||||
updatedMedia = sequelize.models.book.getFromOld(oldLibraryItem.media)
|
||||
|
||||
const existingAuthors = libraryItemExpanded.media.authors || []
|
||||
const existingSeriesAll = libraryItemExpanded.media.series || []
|
||||
const updatedAuthors = oldLibraryItem.media.metadata.authors || []
|
||||
const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||
|
||||
for (const existingAuthor of existingAuthors) {
|
||||
// Author was removed from Book
|
||||
if (!updatedAuthors.some(au => au.id === existingAuthor.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
|
||||
await sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const updatedAuthor of updatedAuthors) {
|
||||
// Author was added
|
||||
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
|
||||
await sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const existingSeries of existingSeriesAll) {
|
||||
// Series was removed
|
||||
if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
|
||||
await sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const updatedSeries of updatedSeriesAll) {
|
||||
// Series was added/updated
|
||||
const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
|
||||
if (!existingSeriesMatch) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
|
||||
await sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
|
||||
hasUpdates = true
|
||||
} else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
|
||||
await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence })
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hasMediaUpdates = false
|
||||
for (const key in updatedMedia) {
|
||||
let existingValue = libraryItemExpanded.media[key]
|
||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||
|
||||
if (!areEquivalent(updatedMedia[key], existingValue, true)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`)
|
||||
hasMediaUpdates = true
|
||||
}
|
||||
}
|
||||
if (hasMediaUpdates && updatedMedia) {
|
||||
await libraryItemExpanded.media.update(updatedMedia)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
const updatedLibraryItem = this.getFromOld(oldLibraryItem)
|
||||
let hasLibraryItemUpdates = false
|
||||
for (const key in updatedLibraryItem) {
|
||||
let existingValue = libraryItemExpanded[key]
|
||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||
|
||||
if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
|
||||
hasLibraryItemUpdates = true
|
||||
}
|
||||
}
|
||||
if (hasLibraryItemUpdates) {
|
||||
await libraryItemExpanded.update(updatedLibraryItem)
|
||||
hasUpdates = true
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
static updateFromOld(oldLibraryItem) {
|
||||
const libraryItem = this.getFromOld(oldLibraryItem)
|
||||
return this.update(libraryItem, {
|
||||
where: {
|
||||
id: libraryItem.id
|
||||
}
|
||||
}).then((result) => result[0] > 0).catch((error) => {
|
||||
Logger.error(`[LibraryItem] Failed to update libraryItem ${libraryItem.id}`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldLibraryItem) {
|
||||
return {
|
||||
id: oldLibraryItem.id,
|
||||
ino: oldLibraryItem.ino,
|
||||
path: oldLibraryItem.path,
|
||||
relPath: oldLibraryItem.relPath,
|
||||
mediaId: oldLibraryItem.media.id,
|
||||
mediaType: oldLibraryItem.mediaType,
|
||||
isFile: !!oldLibraryItem.isFile,
|
||||
isMissing: !!oldLibraryItem.isMissing,
|
||||
isInvalid: !!oldLibraryItem.isInvalid,
|
||||
mtime: oldLibraryItem.mtimeMs,
|
||||
ctime: oldLibraryItem.ctimeMs,
|
||||
birthtime: oldLibraryItem.birthtimeMs,
|
||||
lastScan: oldLibraryItem.lastScan,
|
||||
lastScanVersion: oldLibraryItem.scanVersion,
|
||||
createdAt: oldLibraryItem.addedAt,
|
||||
updatedAt: oldLibraryItem.updatedAt,
|
||||
libraryId: oldLibraryItem.libraryId,
|
||||
libraryFolderId: oldLibraryItem.folderId,
|
||||
libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || []
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(libraryItemId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: libraryItemId
|
||||
},
|
||||
individualHooks: true
|
||||
})
|
||||
}
|
||||
|
||||
getMedia(options) {
|
||||
if (!this.mediaType) return Promise.resolve(null)
|
||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
}
|
||||
|
||||
LibraryItem.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
ino: DataTypes.STRING,
|
||||
path: DataTypes.STRING,
|
||||
relPath: DataTypes.STRING,
|
||||
mediaId: DataTypes.UUIDV4,
|
||||
mediaType: DataTypes.STRING,
|
||||
isFile: DataTypes.BOOLEAN,
|
||||
isMissing: DataTypes.BOOLEAN,
|
||||
isInvalid: DataTypes.BOOLEAN,
|
||||
mtime: DataTypes.DATE(6),
|
||||
ctime: DataTypes.DATE(6),
|
||||
birthtime: DataTypes.DATE(6),
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
libraryFiles: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'libraryItem'
|
||||
})
|
||||
|
||||
const { library, libraryFolder, book, podcast } = sequelize.models
|
||||
library.hasMany(LibraryItem)
|
||||
LibraryItem.belongsTo(library)
|
||||
|
||||
libraryFolder.hasMany(LibraryItem)
|
||||
LibraryItem.belongsTo(libraryFolder)
|
||||
|
||||
book.hasOne(LibraryItem, {
|
||||
foreignKey: 'mediaId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaType: 'book'
|
||||
}
|
||||
})
|
||||
LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
|
||||
|
||||
podcast.hasOne(LibraryItem, {
|
||||
foreignKey: 'mediaId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaType: 'podcast'
|
||||
}
|
||||
})
|
||||
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
||||
|
||||
LibraryItem.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
for (const instance of findResult) {
|
||||
if (instance.mediaType === 'book' && instance.book !== undefined) {
|
||||
instance.media = instance.book
|
||||
instance.dataValues.media = instance.dataValues.book
|
||||
} else if (instance.mediaType === 'podcast' && instance.podcast !== undefined) {
|
||||
instance.media = instance.podcast
|
||||
instance.dataValues.media = instance.dataValues.podcast
|
||||
}
|
||||
// To prevent mistakes:
|
||||
delete instance.book
|
||||
delete instance.dataValues.book
|
||||
delete instance.podcast
|
||||
delete instance.dataValues.podcast
|
||||
}
|
||||
})
|
||||
|
||||
LibraryItem.addHook('afterDestroy', async instance => {
|
||||
if (!instance) return
|
||||
const media = await instance.getMedia()
|
||||
if (media) {
|
||||
media.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
return LibraryItem
|
||||
}
|
141
server/models/MediaProgress.js
Normal file
141
server/models/MediaProgress.js
Normal file
@ -0,0 +1,141 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
/*
|
||||
* Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||
* Book has many MediaProgress. PodcastEpisode has many MediaProgress.
|
||||
*/
|
||||
module.exports = (sequelize) => {
|
||||
class MediaProgress extends Model {
|
||||
getOldMediaProgress() {
|
||||
const isPodcastEpisode = this.mediaItemType === 'podcastEpisode'
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
libraryItemId: this.extraData?.libraryItemId || null,
|
||||
episodeId: isPodcastEpisode ? this.mediaItemId : null,
|
||||
mediaItemId: this.mediaItemId,
|
||||
mediaItemType: this.mediaItemType,
|
||||
duration: this.duration,
|
||||
progress: this.extraData?.progress || null,
|
||||
currentTime: this.currentTime,
|
||||
isFinished: !!this.isFinished,
|
||||
hideFromContinueListening: !!this.hideFromContinueListening,
|
||||
ebookLocation: this.ebookLocation,
|
||||
ebookProgress: this.ebookProgress,
|
||||
lastUpdate: this.updatedAt.valueOf(),
|
||||
startedAt: this.createdAt.valueOf(),
|
||||
finishedAt: this.finishedAt?.valueOf() || null
|
||||
}
|
||||
}
|
||||
|
||||
static upsertFromOld(oldMediaProgress) {
|
||||
const mediaProgress = this.getFromOld(oldMediaProgress)
|
||||
return this.upsert(mediaProgress)
|
||||
}
|
||||
|
||||
static getFromOld(oldMediaProgress) {
|
||||
return {
|
||||
id: oldMediaProgress.id,
|
||||
userId: oldMediaProgress.userId,
|
||||
mediaItemId: oldMediaProgress.mediaItemId,
|
||||
mediaItemType: oldMediaProgress.mediaItemType,
|
||||
duration: oldMediaProgress.duration,
|
||||
currentTime: oldMediaProgress.currentTime,
|
||||
ebookLocation: oldMediaProgress.ebookLocation || null,
|
||||
ebookProgress: oldMediaProgress.ebookProgress || null,
|
||||
isFinished: !!oldMediaProgress.isFinished,
|
||||
hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening,
|
||||
finishedAt: oldMediaProgress.finishedAt,
|
||||
createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate,
|
||||
updatedAt: oldMediaProgress.lastUpdate,
|
||||
extraData: {
|
||||
libraryItemId: oldMediaProgress.libraryItemId,
|
||||
progress: oldMediaProgress.progress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(mediaProgressId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: mediaProgressId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getMediaItem(options) {
|
||||
if (!this.mediaItemType) return Promise.resolve(null)
|
||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MediaProgress.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
isFinished: DataTypes.BOOLEAN,
|
||||
hideFromContinueListening: DataTypes.BOOLEAN,
|
||||
ebookLocation: DataTypes.STRING,
|
||||
ebookProgress: DataTypes.FLOAT,
|
||||
finishedAt: DataTypes.DATE,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'mediaProgress'
|
||||
})
|
||||
|
||||
const { book, podcastEpisode, user } = sequelize.models
|
||||
|
||||
book.hasMany(MediaProgress, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'book'
|
||||
}
|
||||
})
|
||||
MediaProgress.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
podcastEpisode.hasMany(MediaProgress, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'podcastEpisode'
|
||||
}
|
||||
})
|
||||
MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
MediaProgress.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
||||
for (const instance of findResult) {
|
||||
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||
instance.mediaItem = instance.book
|
||||
instance.dataValues.mediaItem = instance.dataValues.book
|
||||
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||
instance.mediaItem = instance.podcastEpisode
|
||||
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||
}
|
||||
// To prevent mistakes:
|
||||
delete instance.book
|
||||
delete instance.dataValues.book
|
||||
delete instance.podcastEpisode
|
||||
delete instance.dataValues.podcastEpisode
|
||||
}
|
||||
})
|
||||
|
||||
user.hasMany(MediaProgress)
|
||||
MediaProgress.belongsTo(user)
|
||||
|
||||
return MediaProgress
|
||||
}
|
198
server/models/PlaybackSession.js
Normal file
198
server/models/PlaybackSession.js
Normal file
@ -0,0 +1,198 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldPlaybackSession = require('../objects/PlaybackSession')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class PlaybackSession extends Model {
|
||||
static async getOldPlaybackSessions(where = null) {
|
||||
const playbackSessions = await this.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.device
|
||||
}
|
||||
]
|
||||
})
|
||||
return playbackSessions.map(session => this.getOldPlaybackSession(session))
|
||||
}
|
||||
|
||||
static async getById(sessionId) {
|
||||
const playbackSession = await this.findByPk(sessionId, {
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.device
|
||||
}
|
||||
]
|
||||
})
|
||||
if (!playbackSession) return null
|
||||
return this.getOldPlaybackSession(playbackSession)
|
||||
}
|
||||
|
||||
static getOldPlaybackSession(playbackSessionExpanded) {
|
||||
const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode'
|
||||
|
||||
return new oldPlaybackSession({
|
||||
id: playbackSessionExpanded.id,
|
||||
userId: playbackSessionExpanded.userId,
|
||||
libraryId: playbackSessionExpanded.libraryId,
|
||||
libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null,
|
||||
bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId,
|
||||
episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null,
|
||||
mediaType: isPodcastEpisode ? 'podcast' : 'book',
|
||||
mediaMetadata: playbackSessionExpanded.mediaMetadata,
|
||||
chapters: null,
|
||||
displayTitle: playbackSessionExpanded.displayTitle,
|
||||
displayAuthor: playbackSessionExpanded.displayAuthor,
|
||||
coverPath: playbackSessionExpanded.coverPath,
|
||||
duration: playbackSessionExpanded.duration,
|
||||
playMethod: playbackSessionExpanded.playMethod,
|
||||
mediaPlayer: playbackSessionExpanded.mediaPlayer,
|
||||
deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null,
|
||||
serverVersion: playbackSessionExpanded.serverVersion,
|
||||
date: playbackSessionExpanded.date,
|
||||
dayOfWeek: playbackSessionExpanded.dayOfWeek,
|
||||
timeListening: playbackSessionExpanded.timeListening,
|
||||
startTime: playbackSessionExpanded.startTime,
|
||||
currentTime: playbackSessionExpanded.currentTime,
|
||||
startedAt: playbackSessionExpanded.createdAt.valueOf(),
|
||||
updatedAt: playbackSessionExpanded.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static removeById(sessionId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: sessionId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldPlaybackSession) {
|
||||
const playbackSession = this.getFromOld(oldPlaybackSession)
|
||||
return this.create(playbackSession)
|
||||
}
|
||||
|
||||
static updateFromOld(oldPlaybackSession) {
|
||||
const playbackSession = this.getFromOld(oldPlaybackSession)
|
||||
return this.update(playbackSession, {
|
||||
where: {
|
||||
id: playbackSession.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldPlaybackSession) {
|
||||
return {
|
||||
id: oldPlaybackSession.id,
|
||||
mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId,
|
||||
mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book',
|
||||
libraryId: oldPlaybackSession.libraryId,
|
||||
displayTitle: oldPlaybackSession.displayTitle,
|
||||
displayAuthor: oldPlaybackSession.displayAuthor,
|
||||
duration: oldPlaybackSession.duration,
|
||||
playMethod: oldPlaybackSession.playMethod,
|
||||
mediaPlayer: oldPlaybackSession.mediaPlayer,
|
||||
startTime: oldPlaybackSession.startTime,
|
||||
currentTime: oldPlaybackSession.currentTime,
|
||||
serverVersion: oldPlaybackSession.serverVersion || null,
|
||||
createdAt: oldPlaybackSession.startedAt,
|
||||
updatedAt: oldPlaybackSession.updatedAt,
|
||||
userId: oldPlaybackSession.userId,
|
||||
deviceId: oldPlaybackSession.deviceInfo?.id || null,
|
||||
timeListening: oldPlaybackSession.timeListening,
|
||||
coverPath: oldPlaybackSession.coverPath,
|
||||
mediaMetadata: oldPlaybackSession.mediaMetadata,
|
||||
date: oldPlaybackSession.date,
|
||||
dayOfWeek: oldPlaybackSession.dayOfWeek,
|
||||
extraData: {
|
||||
libraryItemId: oldPlaybackSession.libraryItemId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMediaItem(options) {
|
||||
if (!this.mediaItemType) return Promise.resolve(null)
|
||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackSession.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
displayTitle: DataTypes.STRING,
|
||||
displayAuthor: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
playMethod: DataTypes.INTEGER,
|
||||
mediaPlayer: DataTypes.STRING,
|
||||
startTime: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
serverVersion: DataTypes.STRING,
|
||||
coverPath: DataTypes.STRING,
|
||||
timeListening: DataTypes.INTEGER,
|
||||
mediaMetadata: DataTypes.JSON,
|
||||
date: DataTypes.STRING,
|
||||
dayOfWeek: DataTypes.STRING,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'playbackSession'
|
||||
})
|
||||
|
||||
const { book, podcastEpisode, user, device, library } = sequelize.models
|
||||
|
||||
user.hasMany(PlaybackSession)
|
||||
PlaybackSession.belongsTo(user)
|
||||
|
||||
device.hasMany(PlaybackSession)
|
||||
PlaybackSession.belongsTo(device)
|
||||
|
||||
library.hasMany(PlaybackSession)
|
||||
PlaybackSession.belongsTo(library)
|
||||
|
||||
book.hasMany(PlaybackSession, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'book'
|
||||
}
|
||||
})
|
||||
PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
podcastEpisode.hasOne(PlaybackSession, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'podcastEpisode'
|
||||
}
|
||||
})
|
||||
PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
PlaybackSession.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
||||
for (const instance of findResult) {
|
||||
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||
instance.mediaItem = instance.book
|
||||
instance.dataValues.mediaItem = instance.dataValues.book
|
||||
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||
instance.mediaItem = instance.podcastEpisode
|
||||
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||
}
|
||||
// To prevent mistakes:
|
||||
delete instance.book
|
||||
delete instance.dataValues.book
|
||||
delete instance.podcastEpisode
|
||||
delete instance.dataValues.podcastEpisode
|
||||
}
|
||||
})
|
||||
|
||||
return PlaybackSession
|
||||
}
|
171
server/models/Playlist.js
Normal file
171
server/models/Playlist.js
Normal file
@ -0,0 +1,171 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldPlaylist = require('../objects/Playlist')
|
||||
const { areEquivalent } = require('../utils/index')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Playlist extends Model {
|
||||
static async getOldPlaylists() {
|
||||
const playlists = await this.findAll({
|
||||
include: {
|
||||
model: sequelize.models.playlistMediaItem,
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.book,
|
||||
include: sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: sequelize.models.podcastEpisode,
|
||||
include: {
|
||||
model: sequelize.models.podcast,
|
||||
include: sequelize.models.libraryItem
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
return playlists.map(p => this.getOldPlaylist(p))
|
||||
}
|
||||
|
||||
static getOldPlaylist(playlistExpanded) {
|
||||
const items = playlistExpanded.playlistMediaItems.map(pmi => {
|
||||
const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null
|
||||
if (!libraryItemId) {
|
||||
console.log(JSON.stringify(pmi, null, 2))
|
||||
throw new Error('No library item id')
|
||||
}
|
||||
return {
|
||||
episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '',
|
||||
libraryItemId: libraryItemId
|
||||
}
|
||||
})
|
||||
return new oldPlaylist({
|
||||
id: playlistExpanded.id,
|
||||
libraryId: playlistExpanded.libraryId,
|
||||
userId: playlistExpanded.userId,
|
||||
name: playlistExpanded.name,
|
||||
description: playlistExpanded.description,
|
||||
items,
|
||||
lastUpdate: playlistExpanded.updatedAt.valueOf(),
|
||||
createdAt: playlistExpanded.createdAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldPlaylist) {
|
||||
const playlist = this.getFromOld(oldPlaylist)
|
||||
return this.create(playlist)
|
||||
}
|
||||
|
||||
static async fullUpdateFromOld(oldPlaylist, playlistMediaItems) {
|
||||
const existingPlaylist = await this.findByPk(oldPlaylist.id, {
|
||||
include: sequelize.models.playlistMediaItem
|
||||
})
|
||||
if (!existingPlaylist) return false
|
||||
|
||||
let hasUpdates = false
|
||||
const playlist = this.getFromOld(oldPlaylist)
|
||||
|
||||
for (const pmi of playlistMediaItems) {
|
||||
const existingPmi = existingPlaylist.playlistMediaItems.find(i => i.mediaItemId === pmi.mediaItemId)
|
||||
if (!existingPmi) {
|
||||
await sequelize.models.playlistMediaItem.create(pmi)
|
||||
hasUpdates = true
|
||||
} else if (existingPmi.order != pmi.order) {
|
||||
await existingPmi.update({ order: pmi.order })
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const pmi of existingPlaylist.playlistMediaItems) {
|
||||
// Pmi was removed
|
||||
if (!playlistMediaItems.some(i => i.mediaItemId === pmi.mediaItemId)) {
|
||||
await pmi.destroy()
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
let hasPlaylistUpdates = false
|
||||
for (const key in playlist) {
|
||||
let existingValue = existingPlaylist[key]
|
||||
if (existingValue instanceof Date) existingValue = existingValue.valueOf()
|
||||
|
||||
if (!areEquivalent(playlist[key], existingValue)) {
|
||||
hasPlaylistUpdates = true
|
||||
}
|
||||
}
|
||||
if (hasPlaylistUpdates) {
|
||||
existingPlaylist.update(playlist)
|
||||
hasUpdates = true
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
static getFromOld(oldPlaylist) {
|
||||
return {
|
||||
id: oldPlaylist.id,
|
||||
name: oldPlaylist.name,
|
||||
description: oldPlaylist.description,
|
||||
createdAt: oldPlaylist.createdAt,
|
||||
updatedAt: oldPlaylist.lastUpdate,
|
||||
userId: oldPlaylist.userId,
|
||||
libraryId: oldPlaylist.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(playlistId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: playlistId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'playlist'
|
||||
})
|
||||
|
||||
const { library, user } = sequelize.models
|
||||
library.hasMany(Playlist)
|
||||
Playlist.belongsTo(library)
|
||||
|
||||
user.hasMany(Playlist)
|
||||
Playlist.belongsTo(user)
|
||||
|
||||
Playlist.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
||||
for (const instance of findResult) {
|
||||
if (instance.playlistMediaItems?.length) {
|
||||
instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => {
|
||||
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
||||
pmi.mediaItem = pmi.book
|
||||
pmi.dataValues.mediaItem = pmi.dataValues.book
|
||||
} else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) {
|
||||
pmi.mediaItem = pmi.podcastEpisode
|
||||
pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode
|
||||
}
|
||||
// To prevent mistakes:
|
||||
delete pmi.book
|
||||
delete pmi.dataValues.book
|
||||
delete pmi.podcastEpisode
|
||||
delete pmi.dataValues.podcastEpisode
|
||||
return pmi
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
return Playlist
|
||||
}
|
82
server/models/PlaylistMediaItem.js
Normal file
82
server/models/PlaylistMediaItem.js
Normal file
@ -0,0 +1,82 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class PlaylistMediaItem extends Model {
|
||||
static removeByIds(playlistId, mediaItemId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
playlistId,
|
||||
mediaItemId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getMediaItem(options) {
|
||||
if (!this.mediaItemType) return Promise.resolve(null)
|
||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaItemType)}`
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
}
|
||||
|
||||
PlaylistMediaItem.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
order: DataTypes.INTEGER
|
||||
}, {
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'playlistMediaItem'
|
||||
})
|
||||
|
||||
const { book, podcastEpisode, playlist } = sequelize.models
|
||||
|
||||
book.hasMany(PlaylistMediaItem, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'book'
|
||||
}
|
||||
})
|
||||
PlaylistMediaItem.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
podcastEpisode.hasOne(PlaylistMediaItem, {
|
||||
foreignKey: 'mediaItemId',
|
||||
constraints: false,
|
||||
scope: {
|
||||
mediaItemType: 'podcastEpisode'
|
||||
}
|
||||
})
|
||||
PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
PlaylistMediaItem.addHook('afterFind', findResult => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
||||
for (const instance of findResult) {
|
||||
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
||||
instance.mediaItem = instance.book
|
||||
instance.dataValues.mediaItem = instance.dataValues.book
|
||||
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
||||
instance.mediaItem = instance.podcastEpisode
|
||||
instance.dataValues.mediaItem = instance.dataValues.podcastEpisode
|
||||
}
|
||||
// To prevent mistakes:
|
||||
delete instance.book
|
||||
delete instance.dataValues.book
|
||||
delete instance.podcastEpisode
|
||||
delete instance.dataValues.podcastEpisode
|
||||
}
|
||||
})
|
||||
|
||||
playlist.hasMany(PlaylistMediaItem)
|
||||
PlaylistMediaItem.belongsTo(playlist)
|
||||
|
||||
return PlaylistMediaItem
|
||||
}
|
98
server/models/Podcast.js
Normal file
98
server/models/Podcast.js
Normal file
@ -0,0 +1,98 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Podcast extends Model {
|
||||
static getOldPodcast(libraryItemExpanded) {
|
||||
const podcastExpanded = libraryItemExpanded.media
|
||||
const podcastEpisodes = podcastExpanded.podcastEpisodes.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id)).sort((a, b) => a.index - b.index)
|
||||
return {
|
||||
id: podcastExpanded.id,
|
||||
libraryItemId: libraryItemExpanded.id,
|
||||
metadata: {
|
||||
title: podcastExpanded.title,
|
||||
author: podcastExpanded.author,
|
||||
description: podcastExpanded.description,
|
||||
releaseDate: podcastExpanded.releaseDate,
|
||||
genres: podcastExpanded.genres,
|
||||
feedUrl: podcastExpanded.feedURL,
|
||||
imageUrl: podcastExpanded.imageURL,
|
||||
itunesPageUrl: podcastExpanded.itunesPageURL,
|
||||
itunesId: podcastExpanded.itunesId,
|
||||
itunesArtistId: podcastExpanded.itunesArtistId,
|
||||
explicit: podcastExpanded.explicit,
|
||||
language: podcastExpanded.language,
|
||||
type: podcastExpanded.podcastType
|
||||
},
|
||||
coverPath: podcastExpanded.coverPath,
|
||||
tags: podcastExpanded.tags,
|
||||
episodes: podcastEpisodes,
|
||||
autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: podcastExpanded.autoDownloadSchedule,
|
||||
lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null,
|
||||
maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload
|
||||
}
|
||||
}
|
||||
|
||||
static getFromOld(oldPodcast) {
|
||||
const oldPodcastMetadata = oldPodcast.metadata
|
||||
return {
|
||||
id: oldPodcast.id,
|
||||
title: oldPodcastMetadata.title,
|
||||
author: oldPodcastMetadata.author,
|
||||
releaseDate: oldPodcastMetadata.releaseDate,
|
||||
feedURL: oldPodcastMetadata.feedUrl,
|
||||
imageURL: oldPodcastMetadata.imageUrl,
|
||||
description: oldPodcastMetadata.description,
|
||||
itunesPageURL: oldPodcastMetadata.itunesPageUrl,
|
||||
itunesId: oldPodcastMetadata.itunesId,
|
||||
itunesArtistId: oldPodcastMetadata.itunesArtistId,
|
||||
language: oldPodcastMetadata.language,
|
||||
podcastType: oldPodcastMetadata.type,
|
||||
explicit: !!oldPodcastMetadata.explicit,
|
||||
autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
|
||||
lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
|
||||
coverPath: oldPodcast.coverPath,
|
||||
tags: oldPodcast.tags,
|
||||
genres: oldPodcastMetadata.genres
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Podcast.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
releaseDate: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
itunesPageURL: DataTypes.STRING,
|
||||
itunesId: DataTypes.STRING,
|
||||
itunesArtistId: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
|
||||
autoDownloadEpisodes: DataTypes.BOOLEAN,
|
||||
autoDownloadSchedule: DataTypes.STRING,
|
||||
lastEpisodeCheck: DataTypes.DATE,
|
||||
maxEpisodesToKeep: DataTypes.INTEGER,
|
||||
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||
coverPath: DataTypes.STRING,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'podcast'
|
||||
})
|
||||
|
||||
return Podcast
|
||||
}
|
95
server/models/PodcastEpisode.js
Normal file
95
server/models/PodcastEpisode.js
Normal file
@ -0,0 +1,95 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class PodcastEpisode extends Model {
|
||||
getOldPodcastEpisode(libraryItemId = null) {
|
||||
let enclosure = null
|
||||
if (this.enclosureURL) {
|
||||
enclosure = {
|
||||
url: this.enclosureURL,
|
||||
type: this.enclosureType,
|
||||
length: this.enclosureSize !== null ? String(this.enclosureSize) : null
|
||||
}
|
||||
}
|
||||
return {
|
||||
libraryItemId: libraryItemId || null,
|
||||
podcastId: this.podcastId,
|
||||
id: this.id,
|
||||
index: this.index,
|
||||
season: this.season,
|
||||
episode: this.episode,
|
||||
episodeType: this.episodeType,
|
||||
title: this.title,
|
||||
subtitle: this.subtitle,
|
||||
description: this.description,
|
||||
enclosure,
|
||||
pubDate: this.pubDate,
|
||||
chapters: this.chapters,
|
||||
audioFile: this.audioFile,
|
||||
publishedAt: this.publishedAt?.valueOf() || null,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf()
|
||||
}
|
||||
}
|
||||
|
||||
static createFromOld(oldEpisode) {
|
||||
const podcastEpisode = this.getFromOld(oldEpisode)
|
||||
return this.create(podcastEpisode)
|
||||
}
|
||||
|
||||
static getFromOld(oldEpisode) {
|
||||
return {
|
||||
id: oldEpisode.id,
|
||||
index: oldEpisode.index,
|
||||
season: oldEpisode.season,
|
||||
episode: oldEpisode.episode,
|
||||
episodeType: oldEpisode.episodeType,
|
||||
title: oldEpisode.title,
|
||||
subtitle: oldEpisode.subtitle,
|
||||
description: oldEpisode.description,
|
||||
pubDate: oldEpisode.pubDate,
|
||||
enclosureURL: oldEpisode.enclosure?.url || null,
|
||||
enclosureSize: oldEpisode.enclosure?.length || null,
|
||||
enclosureType: oldEpisode.enclosure?.type || null,
|
||||
publishedAt: oldEpisode.publishedAt,
|
||||
createdAt: oldEpisode.addedAt,
|
||||
updatedAt: oldEpisode.updatedAt,
|
||||
podcastId: oldEpisode.podcastId,
|
||||
audioFile: oldEpisode.audioFile?.toJSON() || null,
|
||||
chapters: oldEpisode.chapters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PodcastEpisode.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
index: DataTypes.INTEGER,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING(1000),
|
||||
description: DataTypes.TEXT,
|
||||
pubDate: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
enclosureType: DataTypes.STRING,
|
||||
publishedAt: DataTypes.DATE,
|
||||
|
||||
audioFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'podcastEpisode'
|
||||
})
|
||||
|
||||
const { podcast } = sequelize.models
|
||||
podcast.hasMany(PodcastEpisode)
|
||||
PodcastEpisode.belongsTo(podcast)
|
||||
|
||||
return PodcastEpisode
|
||||
}
|
72
server/models/Series.js
Normal file
72
server/models/Series.js
Normal file
@ -0,0 +1,72 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldSeries = require('../objects/entities/Series')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Series extends Model {
|
||||
static async getAllOldSeries() {
|
||||
const series = await this.findAll()
|
||||
return series.map(se => se.getOldSeries())
|
||||
}
|
||||
|
||||
getOldSeries() {
|
||||
return new oldSeries({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static updateFromOld(oldSeries) {
|
||||
const series = this.getFromOld(oldSeries)
|
||||
return this.update(series, {
|
||||
where: {
|
||||
id: series.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldSeries) {
|
||||
const series = this.getFromOld(oldSeries)
|
||||
return this.create(series)
|
||||
}
|
||||
|
||||
static createBulkFromOld(oldSeriesObjs) {
|
||||
const series = oldSeriesObjs.map(this.getFromOld)
|
||||
return this.bulkCreate(series)
|
||||
}
|
||||
|
||||
static getFromOld(oldSeries) {
|
||||
return {
|
||||
id: oldSeries.id,
|
||||
name: oldSeries.name,
|
||||
description: oldSeries.description
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(seriesId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: seriesId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Series.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'series'
|
||||
})
|
||||
|
||||
return Series
|
||||
}
|
45
server/models/Setting.js
Normal file
45
server/models/Setting.js
Normal file
@ -0,0 +1,45 @@
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
const oldEmailSettings = require('../objects/settings/EmailSettings')
|
||||
const oldServerSettings = require('../objects/settings/ServerSettings')
|
||||
const oldNotificationSettings = require('../objects/settings/NotificationSettings')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class Setting extends Model {
|
||||
static async getOldSettings() {
|
||||
const settings = (await this.findAll()).map(se => se.value)
|
||||
|
||||
|
||||
const emailSettingsJson = settings.find(se => se.id === 'email-settings')
|
||||
const serverSettingsJson = settings.find(se => se.id === 'server-settings')
|
||||
const notificationSettingsJson = settings.find(se => se.id === 'notification-settings')
|
||||
|
||||
return {
|
||||
settings,
|
||||
emailSettings: new oldEmailSettings(emailSettingsJson),
|
||||
serverSettings: new oldServerSettings(serverSettingsJson),
|
||||
notificationSettings: new oldNotificationSettings(notificationSettingsJson)
|
||||
}
|
||||
}
|
||||
|
||||
static updateSettingObj(setting) {
|
||||
return this.upsert({
|
||||
key: setting.id,
|
||||
value: setting
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Setting.init({
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
value: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'setting'
|
||||
})
|
||||
|
||||
return Setting
|
||||
}
|
136
server/models/User.js
Normal file
136
server/models/User.js
Normal file
@ -0,0 +1,136 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const oldUser = require('../objects/user/User')
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
class User extends Model {
|
||||
static async getOldUsers() {
|
||||
const users = await this.findAll({
|
||||
include: sequelize.models.mediaProgress
|
||||
})
|
||||
return users.map(u => this.getOldUser(u))
|
||||
}
|
||||
|
||||
static getOldUser(userExpanded) {
|
||||
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
||||
|
||||
const librariesAccessible = userExpanded.permissions?.librariesAccessible || []
|
||||
const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || []
|
||||
const permissions = userExpanded.permissions || {}
|
||||
delete permissions.librariesAccessible
|
||||
delete permissions.itemTagsSelected
|
||||
|
||||
return new oldUser({
|
||||
id: userExpanded.id,
|
||||
oldUserId: userExpanded.extraData?.oldUserId || null,
|
||||
username: userExpanded.username,
|
||||
pash: userExpanded.pash,
|
||||
type: userExpanded.type,
|
||||
token: userExpanded.token,
|
||||
mediaProgress,
|
||||
seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [],
|
||||
bookmarks: userExpanded.bookmarks,
|
||||
isActive: userExpanded.isActive,
|
||||
isLocked: userExpanded.isLocked,
|
||||
lastSeen: userExpanded.lastSeen?.valueOf() || null,
|
||||
createdAt: userExpanded.createdAt.valueOf(),
|
||||
permissions,
|
||||
librariesAccessible,
|
||||
itemTagsSelected
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldUser) {
|
||||
const user = this.getFromOld(oldUser)
|
||||
return this.create(user)
|
||||
}
|
||||
|
||||
static updateFromOld(oldUser) {
|
||||
const user = this.getFromOld(oldUser)
|
||||
return this.update(user, {
|
||||
where: {
|
||||
id: user.id
|
||||
}
|
||||
}).then((result) => result[0] > 0).catch((error) => {
|
||||
Logger.error(`[User] Failed to save user ${oldUser.id}`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldUser) {
|
||||
return {
|
||||
id: oldUser.id,
|
||||
username: oldUser.username,
|
||||
pash: oldUser.pash || null,
|
||||
type: oldUser.type || null,
|
||||
token: oldUser.token || null,
|
||||
isActive: !!oldUser.isActive,
|
||||
lastSeen: oldUser.lastSeen || null,
|
||||
extraData: {
|
||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||
oldUserId: oldUser.oldUserId
|
||||
},
|
||||
createdAt: oldUser.createdAt || Date.now(),
|
||||
permissions: {
|
||||
...oldUser.permissions,
|
||||
librariesAccessible: oldUser.librariesAccessible || [],
|
||||
itemTagsSelected: oldUser.itemTagsSelected || []
|
||||
},
|
||||
bookmarks: oldUser.bookmarks
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(userId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async createRootUser(username, pash, token) {
|
||||
const newRoot = new oldUser({
|
||||
id: uuidv4(),
|
||||
type: 'root',
|
||||
username,
|
||||
pash,
|
||||
token,
|
||||
isActive: true,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
await this.createFromOld(newRoot)
|
||||
return newRoot
|
||||
}
|
||||
}
|
||||
|
||||
User.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
username: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
pash: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
token: DataTypes.STRING,
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
isLocked: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
lastSeen: DataTypes.DATE,
|
||||
permissions: DataTypes.JSON,
|
||||
bookmarks: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'user'
|
||||
})
|
||||
|
||||
return User
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Collection {
|
||||
constructor(collection) {
|
||||
this.id = null
|
||||
this.libraryId = null
|
||||
this.userId = null
|
||||
|
||||
this.name = null
|
||||
this.description = null
|
||||
@ -25,7 +24,6 @@ class Collection {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryId: this.libraryId,
|
||||
userId: this.userId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
cover: this.cover,
|
||||
@ -60,7 +58,6 @@ class Collection {
|
||||
construct(collection) {
|
||||
this.id = collection.id
|
||||
this.libraryId = collection.libraryId
|
||||
this.userId = collection.userId
|
||||
this.name = collection.name
|
||||
this.description = collection.description || null
|
||||
this.cover = collection.cover || null
|
||||
@ -71,11 +68,10 @@ class Collection {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
if (!data.userId || !data.libraryId || !data.name) {
|
||||
if (!data.libraryId || !data.name) {
|
||||
return false
|
||||
}
|
||||
this.id = getId('col')
|
||||
this.userId = data.userId
|
||||
this.id = uuidv4()
|
||||
this.libraryId = data.libraryId
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
|
@ -1,5 +1,9 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class DeviceInfo {
|
||||
constructor(deviceInfo = null) {
|
||||
this.id = null
|
||||
this.userId = null
|
||||
this.deviceId = null
|
||||
this.ipAddress = null
|
||||
|
||||
@ -16,7 +20,8 @@ class DeviceInfo {
|
||||
this.model = null
|
||||
this.sdkVersion = null // Android Only
|
||||
|
||||
this.serverVersion = null
|
||||
this.clientName = null
|
||||
this.deviceName = null
|
||||
|
||||
if (deviceInfo) {
|
||||
this.construct(deviceInfo)
|
||||
@ -33,6 +38,8 @@ class DeviceInfo {
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
deviceId: this.deviceId,
|
||||
ipAddress: this.ipAddress,
|
||||
browserName: this.browserName,
|
||||
@ -44,7 +51,8 @@ class DeviceInfo {
|
||||
manufacturer: this.manufacturer,
|
||||
model: this.model,
|
||||
sdkVersion: this.sdkVersion,
|
||||
serverVersion: this.serverVersion
|
||||
clientName: this.clientName,
|
||||
deviceName: this.deviceName
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (obj[key] === null || obj[key] === undefined) {
|
||||
@ -65,6 +73,7 @@ class DeviceInfo {
|
||||
// When client doesn't send a device id
|
||||
getTempDeviceId() {
|
||||
const keys = [
|
||||
this.userId,
|
||||
this.browserName,
|
||||
this.browserVersion,
|
||||
this.osName,
|
||||
@ -78,7 +87,9 @@ class DeviceInfo {
|
||||
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
setData(ip, ua, clientDeviceInfo, serverVersion) {
|
||||
setData(ip, ua, clientDeviceInfo, serverVersion, userId) {
|
||||
this.id = uuidv4()
|
||||
this.userId = userId
|
||||
this.deviceId = clientDeviceInfo?.deviceId || null
|
||||
this.ipAddress = ip || null
|
||||
|
||||
@ -88,16 +99,54 @@ class DeviceInfo {
|
||||
this.osVersion = ua?.os.version || null
|
||||
this.deviceType = ua?.device.type || null
|
||||
|
||||
this.clientVersion = clientDeviceInfo?.clientVersion || null
|
||||
this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion
|
||||
this.manufacturer = clientDeviceInfo?.manufacturer || null
|
||||
this.model = clientDeviceInfo?.model || null
|
||||
this.sdkVersion = clientDeviceInfo?.sdkVersion || null
|
||||
|
||||
this.serverVersion = serverVersion || null
|
||||
this.clientName = clientDeviceInfo?.clientName || null
|
||||
if (this.sdkVersion) {
|
||||
if (!this.clientName) this.clientName = 'Abs Android'
|
||||
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||
} else if (this.model) {
|
||||
if (!this.clientName) this.clientName = 'Abs iOS'
|
||||
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||
} else if (this.osName && this.browserName) {
|
||||
if (!this.clientName) this.clientName = 'Abs Web'
|
||||
this.deviceName = `${this.osName} ${this.osVersion || 'N/A'} ${this.browserName}`
|
||||
} else if (!this.clientName) {
|
||||
this.clientName = 'Unknown'
|
||||
}
|
||||
|
||||
if (!this.deviceId) {
|
||||
this.deviceId = this.getTempDeviceId()
|
||||
}
|
||||
}
|
||||
|
||||
update(deviceInfo) {
|
||||
const deviceInfoJson = deviceInfo.toJSON ? deviceInfo.toJSON() : deviceInfo
|
||||
const existingDeviceInfoJson = this.toJSON()
|
||||
|
||||
let hasUpdates = false
|
||||
for (const key in deviceInfoJson) {
|
||||
if (['id', 'deviceId'].includes(key)) continue
|
||||
|
||||
if (deviceInfoJson[key] !== existingDeviceInfoJson[key]) {
|
||||
this[key] = deviceInfoJson[key]
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in existingDeviceInfoJson) {
|
||||
if (['id', 'deviceId'].includes(key)) continue
|
||||
|
||||
if (existingDeviceInfoJson[key] && !deviceInfoJson[key]) {
|
||||
this[key] = null
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = DeviceInfo
|
@ -1,3 +1,4 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const FeedMeta = require('./FeedMeta')
|
||||
const FeedEpisode = require('./FeedEpisode')
|
||||
const RSS = require('../libs/rss')
|
||||
@ -90,7 +91,7 @@ class Feed {
|
||||
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'libraryItem'
|
||||
@ -179,7 +180,7 @@ class Feed {
|
||||
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'collection'
|
||||
@ -253,7 +254,7 @@ class Feed {
|
||||
const libraryId = itemsWithTracks[0].libraryId
|
||||
const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'series'
|
||||
|
@ -1,4 +1,3 @@
|
||||
const Path = require('path')
|
||||
const date = require('../libs/dateAndTime')
|
||||
const { secondsToTimestamp } = require('../utils/index')
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { getId } = require("../utils")
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Folder {
|
||||
constructor(folder = null) {
|
||||
@ -29,7 +29,7 @@ class Folder {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = data.id ? data.id : getId('fol')
|
||||
this.id = data.id || uuidv4()
|
||||
this.fullPath = data.fullPath
|
||||
this.libraryId = data.libraryId
|
||||
this.addedAt = Date.now()
|
||||
|
@ -1,6 +1,6 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const Folder = require('./Folder')
|
||||
const LibrarySettings = require('./settings/LibrarySettings')
|
||||
const { getId } = require('../utils/index')
|
||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||
|
||||
class Library {
|
||||
@ -87,7 +87,7 @@ class Library {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = data.id ? data.id : getId('lib')
|
||||
this.id = data.id || uuidv4()
|
||||
this.name = data.name
|
||||
if (data.folder) {
|
||||
this.folders = [
|
||||
|
@ -1,3 +1,4 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Path = require('path')
|
||||
const { version } = require('../../package.json')
|
||||
@ -8,7 +9,7 @@ const Book = require('./mediaTypes/Book')
|
||||
const Podcast = require('./mediaTypes/Podcast')
|
||||
const Video = require('./mediaTypes/Video')
|
||||
const Music = require('./mediaTypes/Music')
|
||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index')
|
||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||
|
||||
class LibraryItem {
|
||||
@ -191,7 +192,7 @@ class LibraryItem {
|
||||
|
||||
// Data comes from scandir library item data
|
||||
setData(libraryMediaType, payload) {
|
||||
this.id = getId('li')
|
||||
this.id = uuidv4()
|
||||
this.mediaType = libraryMediaType
|
||||
if (libraryMediaType === 'video') {
|
||||
this.media = new Video()
|
||||
@ -202,6 +203,7 @@ class LibraryItem {
|
||||
} else if (libraryMediaType === 'music') {
|
||||
this.media = new Music()
|
||||
}
|
||||
this.media.id = uuidv4()
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
for (const key in payload) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Notification {
|
||||
constructor(notification = null) {
|
||||
@ -57,7 +57,7 @@ class Notification {
|
||||
}
|
||||
|
||||
setData(payload) {
|
||||
this.id = getId('noti')
|
||||
this.id = uuidv4()
|
||||
this.libraryId = payload.libraryId || null
|
||||
this.eventName = payload.eventName
|
||||
this.urls = payload.urls
|
||||
|
@ -1,5 +1,6 @@
|
||||
const date = require('../libs/dateAndTime')
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const serverVersion = require('../../package.json').version
|
||||
const BookMetadata = require('./metadata/BookMetadata')
|
||||
const PodcastMetadata = require('./metadata/PodcastMetadata')
|
||||
const DeviceInfo = require('./DeviceInfo')
|
||||
@ -11,6 +12,7 @@ class PlaybackSession {
|
||||
this.userId = null
|
||||
this.libraryId = null
|
||||
this.libraryItemId = null
|
||||
this.bookId = null
|
||||
this.episodeId = null
|
||||
|
||||
this.mediaType = null
|
||||
@ -24,6 +26,7 @@ class PlaybackSession {
|
||||
this.playMethod = null
|
||||
this.mediaPlayer = null
|
||||
this.deviceInfo = null
|
||||
this.serverVersion = null
|
||||
|
||||
this.date = null
|
||||
this.dayOfWeek = null
|
||||
@ -52,6 +55,7 @@ class PlaybackSession {
|
||||
userId: this.userId,
|
||||
libraryId: this.libraryId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
bookId: this.bookId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||
@ -63,6 +67,7 @@ class PlaybackSession {
|
||||
playMethod: this.playMethod,
|
||||
mediaPlayer: this.mediaPlayer,
|
||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||
serverVersion: this.serverVersion,
|
||||
date: this.date,
|
||||
dayOfWeek: this.dayOfWeek,
|
||||
timeListening: this.timeListening,
|
||||
@ -79,6 +84,7 @@ class PlaybackSession {
|
||||
userId: this.userId,
|
||||
libraryId: this.libraryId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
bookId: this.bookId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||
@ -90,6 +96,7 @@ class PlaybackSession {
|
||||
playMethod: this.playMethod,
|
||||
mediaPlayer: this.mediaPlayer,
|
||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||
serverVersion: this.serverVersion,
|
||||
date: this.date,
|
||||
dayOfWeek: this.dayOfWeek,
|
||||
timeListening: this.timeListening,
|
||||
@ -108,12 +115,20 @@ class PlaybackSession {
|
||||
this.userId = session.userId
|
||||
this.libraryId = session.libraryId || null
|
||||
this.libraryItemId = session.libraryItemId
|
||||
this.bookId = session.bookId
|
||||
this.episodeId = session.episodeId
|
||||
this.mediaType = session.mediaType
|
||||
this.duration = session.duration
|
||||
this.playMethod = session.playMethod
|
||||
this.mediaPlayer = session.mediaPlayer || null
|
||||
|
||||
if (session.deviceInfo instanceof DeviceInfo) {
|
||||
this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON())
|
||||
} else {
|
||||
this.deviceInfo = new DeviceInfo(session.deviceInfo)
|
||||
}
|
||||
|
||||
this.serverVersion = session.serverVersion
|
||||
this.chapters = session.chapters || []
|
||||
|
||||
this.mediaMetadata = null
|
||||
@ -151,7 +166,7 @@ class PlaybackSession {
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this.deviceInfo?.deviceId
|
||||
return this.deviceInfo?.id
|
||||
}
|
||||
|
||||
get deviceDescription() {
|
||||
@ -169,10 +184,11 @@ class PlaybackSession {
|
||||
}
|
||||
|
||||
setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
|
||||
this.id = getId('play')
|
||||
this.id = uuidv4()
|
||||
this.userId = user.id
|
||||
this.libraryId = libraryItem.libraryId
|
||||
this.libraryItemId = libraryItem.id
|
||||
this.bookId = episodeId ? null : libraryItem.media.id
|
||||
this.episodeId = episodeId
|
||||
this.mediaType = libraryItem.mediaType
|
||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||
@ -189,6 +205,7 @@ class PlaybackSession {
|
||||
|
||||
this.mediaPlayer = mediaPlayer
|
||||
this.deviceInfo = deviceInfo || new DeviceInfo()
|
||||
this.serverVersion = serverVersion
|
||||
|
||||
this.timeListening = 0
|
||||
this.startTime = startTime
|
||||
|
@ -1,5 +1,4 @@
|
||||
const Logger = require('../Logger')
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Playlist {
|
||||
constructor(playlist) {
|
||||
@ -88,7 +87,7 @@ class Playlist {
|
||||
if (!data.userId || !data.libraryId || !data.name) {
|
||||
return false
|
||||
}
|
||||
this.id = getId('pl')
|
||||
this.id = uuidv4()
|
||||
this.userId = data.userId
|
||||
this.libraryId = data.libraryId
|
||||
this.name = data.name
|
||||
|
@ -1,5 +1,5 @@
|
||||
const Path = require('path')
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||
const globals = require('../utils/globals')
|
||||
|
||||
@ -70,7 +70,7 @@ class PodcastEpisodeDownload {
|
||||
}
|
||||
|
||||
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
|
||||
this.id = getId('epdl')
|
||||
this.id = uuidv4()
|
||||
this.podcastEpisode = podcastEpisode
|
||||
|
||||
const url = podcastEpisode.enclosure.url
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Task {
|
||||
constructor() {
|
||||
@ -35,7 +35,7 @@ class Task {
|
||||
}
|
||||
|
||||
setData(action, title, description, showSuccess, data = {}) {
|
||||
this.id = getId(action)
|
||||
this.id = uuidv4()
|
||||
this.action = action
|
||||
this.data = { ...data }
|
||||
this.title = title
|
||||
|
@ -1,5 +1,5 @@
|
||||
const Logger = require('../../Logger')
|
||||
const { getId } = require('../../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString')
|
||||
|
||||
class Author {
|
||||
@ -53,7 +53,7 @@ class Author {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = getId('aut')
|
||||
this.id = uuidv4()
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
this.asin = data.asin || null
|
||||
|
@ -1,12 +1,14 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const Path = require('path')
|
||||
const Logger = require('../../Logger')
|
||||
const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||
const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
const AudioTrack = require('../files/AudioTrack')
|
||||
|
||||
class PodcastEpisode {
|
||||
constructor(episode) {
|
||||
this.libraryItemId = null
|
||||
this.podcastId = null
|
||||
this.id = null
|
||||
this.index = null
|
||||
|
||||
@ -32,6 +34,7 @@ class PodcastEpisode {
|
||||
|
||||
construct(episode) {
|
||||
this.libraryItemId = episode.libraryItemId
|
||||
this.podcastId = episode.podcastId
|
||||
this.id = episode.id
|
||||
this.index = episode.index
|
||||
this.season = episode.season
|
||||
@ -54,6 +57,7 @@ class PodcastEpisode {
|
||||
toJSON() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
podcastId: this.podcastId,
|
||||
id: this.id,
|
||||
index: this.index,
|
||||
season: this.season,
|
||||
@ -75,6 +79,7 @@ class PodcastEpisode {
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
podcastId: this.podcastId,
|
||||
id: this.id,
|
||||
index: this.index,
|
||||
season: this.season,
|
||||
@ -117,7 +122,7 @@ class PodcastEpisode {
|
||||
}
|
||||
|
||||
setData(data, index = 1) {
|
||||
this.id = getId('ep')
|
||||
this.id = uuidv4()
|
||||
this.index = index
|
||||
this.title = data.title
|
||||
this.subtitle = data.subtitle || ''
|
||||
@ -133,7 +138,7 @@ class PodcastEpisode {
|
||||
}
|
||||
|
||||
setDataFromAudioFile(audioFile, index) {
|
||||
this.id = getId('ep')
|
||||
this.id = uuidv4()
|
||||
this.audioFile = audioFile
|
||||
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
||||
this.index = index
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { getId } = require('../../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Series {
|
||||
constructor(series) {
|
||||
@ -40,7 +40,7 @@ class Series {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = getId('ser')
|
||||
this.id = uuidv4()
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
this.addedAt = Date.now()
|
||||
|
@ -12,6 +12,7 @@ const EBookFile = require('../files/EBookFile')
|
||||
|
||||
class Book {
|
||||
constructor(book) {
|
||||
this.id = null
|
||||
this.libraryItemId = null
|
||||
this.metadata = null
|
||||
|
||||
@ -32,6 +33,7 @@ class Book {
|
||||
}
|
||||
|
||||
construct(book) {
|
||||
this.id = book.id
|
||||
this.libraryItemId = book.libraryItemId
|
||||
this.metadata = new BookMetadata(book.metadata)
|
||||
this.coverPath = book.coverPath
|
||||
@ -46,6 +48,7 @@ class Book {
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
@ -59,6 +62,7 @@ class Book {
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
id: this.id,
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
@ -75,6 +79,7 @@ class Book {
|
||||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
|
@ -11,6 +11,7 @@ const naturalSort = createNewSortInstance({
|
||||
|
||||
class Podcast {
|
||||
constructor(podcast) {
|
||||
this.id = null
|
||||
this.libraryItemId = null
|
||||
this.metadata = null
|
||||
this.coverPath = null
|
||||
@ -32,6 +33,7 @@ class Podcast {
|
||||
}
|
||||
|
||||
construct(podcast) {
|
||||
this.id = podcast.id
|
||||
this.libraryItemId = podcast.libraryItemId
|
||||
this.metadata = new PodcastMetadata(podcast.metadata)
|
||||
this.coverPath = podcast.coverPath
|
||||
@ -50,6 +52,7 @@ class Podcast {
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
@ -65,6 +68,7 @@ class Podcast {
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
id: this.id,
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
@ -80,6 +84,7 @@ class Podcast {
|
||||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
@ -284,8 +289,9 @@ class Podcast {
|
||||
}
|
||||
|
||||
addNewEpisodeFromAudioFile(audioFile, index) {
|
||||
var pe = new PodcastEpisode()
|
||||
const pe = new PodcastEpisode()
|
||||
pe.libraryItemId = this.libraryItemId
|
||||
pe.podcastId = this.id
|
||||
audioFile.index = 1 // Only 1 audio file per episode
|
||||
pe.setDataFromAudioFile(audioFile, index)
|
||||
this.episodes.push(pe)
|
||||
|
@ -218,7 +218,7 @@ class BookMetadata {
|
||||
|
||||
// Updates author name
|
||||
updateAuthor(updatedAuthor) {
|
||||
var author = this.authors.find(au => au.id === updatedAuthor.id)
|
||||
const author = this.authors.find(au => au.id === updatedAuthor.id)
|
||||
if (!author || author.name == updatedAuthor.name) return false
|
||||
author.name = updatedAuthor.name
|
||||
return true
|
||||
|
@ -1,9 +1,15 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class MediaProgress {
|
||||
constructor(progress) {
|
||||
this.id = null
|
||||
this.userId = null
|
||||
this.libraryItemId = null
|
||||
this.episodeId = null // For podcasts
|
||||
|
||||
this.mediaItemId = null // For use in new data model
|
||||
this.mediaItemType = null // For use in new data model
|
||||
|
||||
this.duration = null
|
||||
this.progress = null // 0 to 1
|
||||
this.currentTime = null // seconds
|
||||
@ -25,8 +31,11 @@ class MediaProgress {
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
episodeId: this.episodeId,
|
||||
mediaItemId: this.mediaItemId,
|
||||
mediaItemType: this.mediaItemType,
|
||||
duration: this.duration,
|
||||
progress: this.progress,
|
||||
currentTime: this.currentTime,
|
||||
@ -42,8 +51,11 @@ class MediaProgress {
|
||||
|
||||
construct(progress) {
|
||||
this.id = progress.id
|
||||
this.userId = progress.userId
|
||||
this.libraryItemId = progress.libraryItemId
|
||||
this.episodeId = progress.episodeId
|
||||
this.mediaItemId = progress.mediaItemId
|
||||
this.mediaItemType = progress.mediaItemType
|
||||
this.duration = progress.duration || 0
|
||||
this.progress = progress.progress
|
||||
this.currentTime = progress.currentTime || 0
|
||||
@ -60,10 +72,16 @@ class MediaProgress {
|
||||
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
||||
}
|
||||
|
||||
setData(libraryItemId, progress, episodeId = null) {
|
||||
this.id = episodeId ? `${libraryItemId}-${episodeId}` : libraryItemId
|
||||
this.libraryItemId = libraryItemId
|
||||
setData(libraryItem, progress, episodeId, userId) {
|
||||
this.id = uuidv4()
|
||||
this.userId = userId
|
||||
this.libraryItemId = libraryItem.id
|
||||
this.episodeId = episodeId
|
||||
|
||||
// PodcastEpisodeId or BookId
|
||||
this.mediaItemId = episodeId || libraryItem.media.id
|
||||
this.mediaItemType = episodeId ? 'podcastEpisode' : 'book'
|
||||
|
||||
this.duration = progress.duration || 0
|
||||
this.progress = Math.min(1, (progress.progress || 0))
|
||||
this.currentTime = progress.currentTime || 0
|
||||
|
@ -5,6 +5,7 @@ const MediaProgress = require('./MediaProgress')
|
||||
class User {
|
||||
constructor(user) {
|
||||
this.id = null
|
||||
this.oldUserId = null // TODO: Temp for keeping old access tokens
|
||||
this.username = null
|
||||
this.pash = null
|
||||
this.type = null
|
||||
@ -73,6 +74,7 @@ class User {
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
pash: this.pash,
|
||||
type: this.type,
|
||||
@ -93,6 +95,7 @@ class User {
|
||||
toJSONForBrowser(hideRootToken = false, minimal = false) {
|
||||
const json = {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
type: this.type,
|
||||
token: (this.type === 'root' && hideRootToken) ? '' : this.token,
|
||||
@ -126,6 +129,7 @@ class User {
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
type: this.type,
|
||||
session,
|
||||
@ -137,6 +141,7 @@ class User {
|
||||
|
||||
construct(user) {
|
||||
this.id = user.id
|
||||
this.oldUserId = user.oldUserId
|
||||
this.username = user.username
|
||||
this.pash = user.pash
|
||||
this.type = user.type
|
||||
@ -320,7 +325,7 @@ class User {
|
||||
if (!itemProgress) {
|
||||
const newItemProgress = new MediaProgress()
|
||||
|
||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
||||
newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id)
|
||||
this.mediaProgress.push(newItemProgress)
|
||||
return true
|
||||
}
|
||||
@ -336,12 +341,6 @@ class User {
|
||||
return true
|
||||
}
|
||||
|
||||
removeMediaProgressForLibraryItem(libraryItemId) {
|
||||
if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false
|
||||
this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId)
|
||||
return true
|
||||
}
|
||||
|
||||
checkCanAccessLibrary(libraryId) {
|
||||
if (this.permissions.accessAllLibraries) return true
|
||||
if (!this.librariesAccessible) return false
|
||||
|
@ -2,6 +2,7 @@ const express = require('express')
|
||||
const Path = require('path')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
@ -37,7 +38,6 @@ const Series = require('../objects/entities/Series')
|
||||
|
||||
class ApiRouter {
|
||||
constructor(Server) {
|
||||
this.db = Server.db
|
||||
this.auth = Server.auth
|
||||
this.scanner = Server.scanner
|
||||
this.playbackSessionManager = Server.playbackSessionManager
|
||||
@ -356,7 +356,7 @@ class ApiRouter {
|
||||
const json = user.toJSONForBrowser(hideRootToken)
|
||||
|
||||
json.mediaProgress = json.mediaProgress.map(lip => {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === lip.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
|
||||
lip.media = null
|
||||
@ -381,11 +381,10 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
async handleDeleteLibraryItem(libraryItem) {
|
||||
// Remove libraryItem from users
|
||||
for (let i = 0; i < this.db.users.length; i++) {
|
||||
const user = this.db.users[i]
|
||||
if (user.removeMediaProgressForLibraryItem(libraryItem.id)) {
|
||||
await this.db.updateEntity('user', user)
|
||||
// Remove media progress for this library item from all users
|
||||
for (const user of Database.users) {
|
||||
for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) {
|
||||
await Database.removeMediaProgress(mediaProgress.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,12 +392,12 @@ class ApiRouter {
|
||||
|
||||
if (libraryItem.isBook) {
|
||||
// remove book from collections
|
||||
const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
|
||||
const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id))
|
||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
||||
const collection = collectionsWithBook[i]
|
||||
collection.removeBook(libraryItem.id)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// Check remove empty series
|
||||
@ -406,7 +405,7 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
// remove item from playlists
|
||||
const playlistsWithItem = this.db.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
|
||||
const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
|
||||
for (let i = 0; i < playlistsWithItem.length; i++) {
|
||||
const playlist = playlistsWithItem[i]
|
||||
playlist.removeItemsForLibraryItem(libraryItem.id)
|
||||
@ -414,11 +413,12 @@ class ApiRouter {
|
||||
// If playlist is now empty then remove it
|
||||
if (!playlist.items.length) {
|
||||
Logger.info(`[ApiRouter] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems))
|
||||
} else {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.updatePlaylist(playlist)
|
||||
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,7 +436,7 @@ class ApiRouter {
|
||||
await fs.remove(itemMetadataPath)
|
||||
}
|
||||
|
||||
await this.db.removeLibraryItem(libraryItem.id)
|
||||
await Database.removeLibraryItem(libraryItem.id)
|
||||
SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -444,27 +444,27 @@ class ApiRouter {
|
||||
if (!seriesToCheck || !seriesToCheck.length) return
|
||||
|
||||
for (const series of seriesToCheck) {
|
||||
const otherLibraryItemsInSeries = this.db.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
||||
const otherLibraryItemsInSeries = Database.libraryItems.filter(li => li.id !== excludeLibraryItemId && li.isBook && li.media.metadata.hasSeries(series.id))
|
||||
if (!otherLibraryItemsInSeries.length) {
|
||||
// Close open RSS feed for series
|
||||
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
||||
Logger.debug(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
||||
await this.db.removeEntity('series', series.id)
|
||||
await Database.removeSeries(series.id)
|
||||
// TODO: Socket events for series?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getUserListeningSessionsHelper(userId) {
|
||||
const userSessions = await this.db.selectUserSessions(userId)
|
||||
const userSessions = await Database.getPlaybackSessions({ userId })
|
||||
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
async getAllSessionsWithUserData() {
|
||||
const sessions = await this.db.getAllSessions()
|
||||
const sessions = await Database.getPlaybackSessions()
|
||||
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return sessions.map(se => {
|
||||
const user = this.db.users.find(u => u.id === se.userId)
|
||||
const user = Database.users.find(u => u.id === se.userId)
|
||||
return {
|
||||
...se,
|
||||
user: user ? { id: user.id, username: user.username } : null
|
||||
@ -533,7 +533,7 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
|
||||
let author = this.db.authors.find(au => au.checkNameEquals(authorName))
|
||||
let author = Database.authors.find(au => au.checkNameEquals(authorName))
|
||||
if (!author) {
|
||||
author = new Author()
|
||||
author.setData(mediaMetadata.authors[i])
|
||||
@ -546,7 +546,7 @@ class ApiRouter {
|
||||
}
|
||||
}
|
||||
if (newAuthors.length) {
|
||||
await this.db.insertEntities('author', newAuthors)
|
||||
await Database.createBulkAuthors(newAuthors)
|
||||
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
||||
}
|
||||
}
|
||||
@ -562,7 +562,7 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
|
||||
let seriesItem = this.db.series.find(se => se.checkNameEquals(seriesName))
|
||||
let seriesItem = Database.series.find(se => se.checkNameEquals(seriesName))
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData(mediaMetadata.series[i])
|
||||
@ -575,7 +575,7 @@ class ApiRouter {
|
||||
}
|
||||
}
|
||||
if (newSeries.length) {
|
||||
await this.db.insertEntities('series', newSeries)
|
||||
await Database.createBulkSeries(newSeries)
|
||||
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,7 @@ const fs = require('../libs/fsExtra')
|
||||
|
||||
|
||||
class HlsRouter {
|
||||
constructor(db, auth, playbackSessionManager) {
|
||||
this.db = db
|
||||
constructor(auth, playbackSessionManager) {
|
||||
this.auth = auth
|
||||
this.playbackSessionManager = playbackSessionManager
|
||||
|
||||
|
8
server/routes/index.js
Normal file
8
server/routes/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
const express = require('express')
|
||||
const libraries = require('./libraries')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/libraries', libraries)
|
||||
|
||||
module.exports = router
|
7
server/routes/libraries.js
Normal file
7
server/routes/libraries.js
Normal file
@ -0,0 +1,7 @@
|
||||
const express = require('express')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// TODO: Add library routes
|
||||
|
||||
module.exports = router
|
@ -1,4 +1,5 @@
|
||||
const Path = require('path')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const fs = require('../libs/fsExtra')
|
||||
const date = require('../libs/dateAndTime')
|
||||
|
||||
@ -6,7 +7,7 @@ const Logger = require('../Logger')
|
||||
const Library = require('../objects/Library')
|
||||
const { LogLevel } = require('../utils/constants')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
const { getId, secondsToTimestamp } = require('../utils/index')
|
||||
const { secondsToTimestamp } = require('../utils/index')
|
||||
|
||||
class LibraryScan {
|
||||
constructor() {
|
||||
@ -84,7 +85,7 @@ class LibraryScan {
|
||||
}
|
||||
|
||||
setData(library, scanOptions, type = 'scan') {
|
||||
this.id = getId('lscan')
|
||||
this.id = uuidv4()
|
||||
this.type = type
|
||||
this.library = new Library(library.toJSON()) // clone library
|
||||
|
||||
|
@ -2,6 +2,7 @@ const fs = require('../libs/fsExtra')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
// Utils
|
||||
const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir')
|
||||
@ -22,8 +23,7 @@ const Series = require('../objects/entities/Series')
|
||||
const Task = require('../objects/Task')
|
||||
|
||||
class Scanner {
|
||||
constructor(db, coverManager, taskManager) {
|
||||
this.db = db
|
||||
constructor(coverManager, taskManager) {
|
||||
this.coverManager = coverManager
|
||||
this.taskManager = taskManager
|
||||
|
||||
@ -66,7 +66,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
async scanLibraryItemByRequest(libraryItem) {
|
||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||
return ScanResult.NOTHING
|
||||
@ -108,7 +108,7 @@ class Scanner {
|
||||
if (checkRes.updated) hasUpdated = true
|
||||
|
||||
// Sync other files first so that local images are used as cover art
|
||||
if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata, library.settings)) {
|
||||
if (await libraryItem.syncFiles(Database.serverSettings.scannerPreferOpfMetadata, library.settings)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
return ScanResult.UPDATED
|
||||
}
|
||||
@ -160,7 +160,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
const scanOptions = new ScanOptions()
|
||||
scanOptions.setData(options, this.db.serverSettings)
|
||||
scanOptions.setData(options, Database.serverSettings)
|
||||
|
||||
const libraryScan = new LibraryScan()
|
||||
libraryScan.setData(library, scanOptions)
|
||||
@ -212,7 +212,7 @@ class Scanner {
|
||||
|
||||
// Remove items with no inode
|
||||
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
|
||||
const libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
||||
const libraryItemsInLibrary = Database.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
||||
|
||||
const MaxSizePerChunk = 2.5e9
|
||||
const itemDataToRescanChunks = []
|
||||
@ -333,7 +333,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
async updateLibraryItemChunk(itemsToUpdate) {
|
||||
await this.db.updateLibraryItems(itemsToUpdate)
|
||||
await Database.updateBulkLibraryItems(itemsToUpdate)
|
||||
SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
@ -351,7 +351,7 @@ class Scanner {
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
libraryScan.resultsUpdated += itemsUpdated.length
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
await Database.updateBulkLibraryItems(itemsUpdated)
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
}
|
||||
@ -368,7 +368,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
libraryScan.resultsAdded += newLibraryItems.length
|
||||
await this.db.insertLibraryItems(newLibraryItems)
|
||||
await Database.createBulkLibraryItems(newLibraryItems)
|
||||
SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
@ -436,6 +436,7 @@ class Scanner {
|
||||
|
||||
const libraryItem = new LibraryItem()
|
||||
libraryItem.setData(library.mediaType, libraryItemData)
|
||||
libraryItem.setLastScan()
|
||||
|
||||
const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video')
|
||||
if (mediaFiles.length) {
|
||||
@ -478,7 +479,7 @@ class Scanner {
|
||||
if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) {
|
||||
var newAuthors = []
|
||||
libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => {
|
||||
var _author = this.db.authors.find(au => au.checkNameEquals(tempMinAuthor.name))
|
||||
var _author = Database.authors.find(au => au.checkNameEquals(tempMinAuthor.name))
|
||||
if (!_author) _author = newAuthors.find(au => au.checkNameEquals(tempMinAuthor.name)) // Check new unsaved authors
|
||||
if (!_author) { // Must create new author
|
||||
_author = new Author()
|
||||
@ -492,14 +493,14 @@ class Scanner {
|
||||
}
|
||||
})
|
||||
if (newAuthors.length) {
|
||||
await this.db.insertEntities('author', newAuthors)
|
||||
await Database.createBulkAuthors(newAuthors)
|
||||
SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
||||
}
|
||||
}
|
||||
if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) {
|
||||
var newSeries = []
|
||||
libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => {
|
||||
var _series = this.db.series.find(se => se.checkNameEquals(tempMinSeries.name))
|
||||
var _series = Database.series.find(se => se.checkNameEquals(tempMinSeries.name))
|
||||
if (!_series) _series = newSeries.find(se => se.checkNameEquals(tempMinSeries.name)) // Check new unsaved series
|
||||
if (!_series) { // Must create new series
|
||||
_series = new Series()
|
||||
@ -513,7 +514,7 @@ class Scanner {
|
||||
}
|
||||
})
|
||||
if (newSeries.length) {
|
||||
await this.db.insertEntities('series', newSeries)
|
||||
await Database.createBulkSeries(newSeries)
|
||||
SocketAuthority.emitter('multiple_series_added', newSeries.map(se => se.toJSON()))
|
||||
}
|
||||
}
|
||||
@ -551,7 +552,7 @@ class Scanner {
|
||||
|
||||
for (const folderId in folderGroups) {
|
||||
const libraryId = folderGroups[folderId].libraryId
|
||||
const library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||
const library = Database.libraries.find(lib => lib.id === libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
||||
continue;
|
||||
@ -597,12 +598,12 @@ class Scanner {
|
||||
const altDir = `${itemDir}/${firstNest}`
|
||||
|
||||
const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir)
|
||||
const childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath))
|
||||
const childLibraryItem = Database.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath))
|
||||
if (!childLibraryItem) {
|
||||
continue
|
||||
}
|
||||
const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir)
|
||||
const altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath))
|
||||
const altChildLibraryItem = Database.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath))
|
||||
if (altChildLibraryItem) {
|
||||
continue
|
||||
}
|
||||
@ -619,9 +620,9 @@ class Scanner {
|
||||
const dirIno = await getIno(fullPath)
|
||||
|
||||
// Check if book dir group is already an item
|
||||
let existingLibraryItem = this.db.libraryItems.find(li => fullPath.startsWith(li.path))
|
||||
let existingLibraryItem = Database.libraryItems.find(li => fullPath.startsWith(li.path))
|
||||
if (!existingLibraryItem) {
|
||||
existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno)
|
||||
existingLibraryItem = Database.libraryItems.find(li => li.ino === dirIno)
|
||||
if (existingLibraryItem) {
|
||||
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`)
|
||||
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
|
||||
@ -636,7 +637,7 @@ class Scanner {
|
||||
if (!exists) {
|
||||
Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`)
|
||||
existingLibraryItem.setMissing()
|
||||
await this.db.updateLibraryItem(existingLibraryItem)
|
||||
await Database.updateLibraryItem(existingLibraryItem)
|
||||
SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded())
|
||||
|
||||
itemGroupingResults[itemDir] = ScanResult.REMOVED
|
||||
@ -654,7 +655,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
// Check if a library item is a subdirectory of this dir
|
||||
var childItem = this.db.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/'))
|
||||
var childItem = Database.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/'))
|
||||
if (childItem) {
|
||||
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
||||
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
||||
@ -666,7 +667,7 @@ class Scanner {
|
||||
var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem)
|
||||
if (newLibraryItem) {
|
||||
await this.createNewAuthorsAndSeries(newLibraryItem)
|
||||
await this.db.insertLibraryItem(newLibraryItem)
|
||||
await Database.createLibraryItem(newLibraryItem)
|
||||
SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded())
|
||||
}
|
||||
itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
|
||||
@ -686,7 +687,7 @@ class Scanner {
|
||||
titleDistance: 2,
|
||||
authorDistance: 2
|
||||
}
|
||||
const scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
|
||||
const scannerCoverProvider = Database.serverSettings.scannerCoverProvider
|
||||
const results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
|
||||
if (results.length) {
|
||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
|
||||
@ -716,7 +717,7 @@ class Scanner {
|
||||
|
||||
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
|
||||
// the overrideDefaults option is not set or set to false.
|
||||
if ((overrideDefaults == false) && (this.db.serverSettings.scannerPreferMatchedMetadata)) {
|
||||
if ((overrideDefaults == false) && (Database.serverSettings.scannerPreferMatchedMetadata)) {
|
||||
options.overrideCover = true
|
||||
options.overrideDetails = true
|
||||
}
|
||||
@ -783,7 +784,7 @@ class Scanner {
|
||||
await this.quickMatchPodcastEpisodes(libraryItem, options)
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
@ -878,11 +879,11 @@ class Scanner {
|
||||
const authorPayload = []
|
||||
for (let index = 0; index < matchData.author.length; index++) {
|
||||
const authorName = matchData.author[index]
|
||||
var author = this.db.authors.find(au => au.checkNameEquals(authorName))
|
||||
var author = Database.authors.find(au => au.checkNameEquals(authorName))
|
||||
if (!author) {
|
||||
author = new Author()
|
||||
author.setData({ name: authorName })
|
||||
await this.db.insertEntity('author', author)
|
||||
await Database.createAuthor(author)
|
||||
SocketAuthority.emitter('author_added', author.toJSON())
|
||||
}
|
||||
authorPayload.push(author.toJSONMinimal())
|
||||
@ -896,11 +897,11 @@ class Scanner {
|
||||
const seriesPayload = []
|
||||
for (let index = 0; index < matchData.series.length; index++) {
|
||||
const seriesMatchItem = matchData.series[index]
|
||||
var seriesItem = this.db.series.find(au => au.checkNameEquals(seriesMatchItem.series))
|
||||
var seriesItem = Database.series.find(au => au.checkNameEquals(seriesMatchItem.series))
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData({ name: seriesMatchItem.series })
|
||||
await this.db.insertEntity('series', seriesItem)
|
||||
await Database.createSeries(seriesItem)
|
||||
SocketAuthority.emitter('series_added', seriesItem.toJSON())
|
||||
}
|
||||
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
|
||||
@ -981,7 +982,7 @@ class Scanner {
|
||||
return
|
||||
}
|
||||
|
||||
var itemsInLibrary = this.db.getLibraryItemsInLibrary(library.id)
|
||||
const itemsInLibrary = Database.libraryItems.filter(li => li.libraryId === library.id)
|
||||
if (!itemsInLibrary.length) {
|
||||
Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`)
|
||||
return
|
||||
|
@ -17,24 +17,31 @@
|
||||
@param value2 Other item to compare
|
||||
@param stack Used internally to track circular refs - don't set it
|
||||
*/
|
||||
module.exports = function areEquivalent(value1, value2, stack = []) {
|
||||
module.exports = function areEquivalent(value1, value2, numToString = false, stack = []) {
|
||||
if (numToString) {
|
||||
if (value1 !== null && !isNaN(value1)) value1 = String(value1)
|
||||
if (value2 !== null && !isNaN(value2)) value2 = String(value2)
|
||||
}
|
||||
|
||||
// Numbers, strings, null, undefined, symbols, functions, booleans.
|
||||
// Also: objects (incl. arrays) that are actually the same instance
|
||||
if (value1 === value2) {
|
||||
// Fast and done
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
// Truthy check to handle value1=null, value2=Object
|
||||
if ((value1 && !value2) || (!value1 && value2)) {
|
||||
console.log('value1/value2 falsy mismatch', value1, value2)
|
||||
return false
|
||||
}
|
||||
|
||||
const type1 = typeof value1;
|
||||
const type1 = typeof value1
|
||||
|
||||
// Ensure types match
|
||||
if (type1 !== typeof value2) {
|
||||
return false;
|
||||
console.log('type diff', type1, typeof value2)
|
||||
return false
|
||||
}
|
||||
|
||||
// Special case for number: check for NaN on both sides
|
||||
@ -49,26 +56,27 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
|
||||
// Failed initial equals test, but could still have equivalent
|
||||
// implementations - note, will match on functions that have same name
|
||||
// and are native code: `function abc() { [native code] }`
|
||||
return value1.toString() === value2.toString();
|
||||
return value1.toString() === value2.toString()
|
||||
}
|
||||
|
||||
// For these types, cannot still be equal at this point, so fast-fail
|
||||
if (type1 === 'bigint' || type1 === 'boolean' ||
|
||||
type1 === 'function' || type1 === 'string' ||
|
||||
type1 === 'symbol') {
|
||||
return false;
|
||||
console.log('no match for values', value1, value2)
|
||||
return false
|
||||
}
|
||||
|
||||
// For dates, cast to number and ensure equal or both NaN (note, if same
|
||||
// exact instance then we're not here - that was checked above)
|
||||
if (value1 instanceof Date) {
|
||||
if (!(value2 instanceof Date)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
// Convert to number to compare
|
||||
const asNum1 = +value1, asNum2 = +value2;
|
||||
const asNum1 = +value1, asNum2 = +value2
|
||||
// Check if both invalid (NaN) or are same value
|
||||
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2));
|
||||
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2))
|
||||
}
|
||||
|
||||
// At this point, it's a reference type and could be circular, so
|
||||
@ -80,61 +88,67 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
|
||||
}
|
||||
|
||||
// breadcrumb
|
||||
stack.push(value1);
|
||||
stack.push(value1)
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value1)) {
|
||||
if (!Array.isArray(value2)) {
|
||||
return false;
|
||||
console.log('value2 is not array but value1 is', value1, value2)
|
||||
return false
|
||||
}
|
||||
|
||||
const length = value1.length;
|
||||
const length = value1.length
|
||||
|
||||
if (length !== value2.length) {
|
||||
return false;
|
||||
console.log('array length diff', length)
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (!areEquivalent(value1[i], value2[i], stack)) {
|
||||
return false;
|
||||
if (!areEquivalent(value1[i], value2[i], numToString, stack)) {
|
||||
console.log('2 array items are not equiv', value1[i], value2[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
// Final case: object
|
||||
|
||||
// get both key lists and check length
|
||||
const keys1 = Object.keys(value1);
|
||||
const keys2 = Object.keys(value2);
|
||||
const numKeys = keys1.length;
|
||||
const keys1 = Object.keys(value1)
|
||||
const keys2 = Object.keys(value2)
|
||||
const numKeys = keys1.length
|
||||
|
||||
if (keys2.length !== numKeys) {
|
||||
return false;
|
||||
console.log('Key length is diff', keys2.length, numKeys)
|
||||
return false
|
||||
}
|
||||
|
||||
// Empty object on both sides?
|
||||
if (numKeys === 0) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
// sort is a native call so it's very fast - much faster than comparing the
|
||||
// values at each key if it can be avoided, so do the sort and then
|
||||
// ensure every key matches at every index
|
||||
keys1.sort();
|
||||
keys2.sort();
|
||||
keys1.sort()
|
||||
keys2.sort()
|
||||
|
||||
// Ensure perfect match across all keys
|
||||
for (let i = 0; i < numKeys; i++) {
|
||||
if (keys1[i] !== keys2[i]) {
|
||||
return false;
|
||||
console.log('object key is not equiv', keys1[i], keys2[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure perfect match across all values
|
||||
for (let i = 0; i < numKeys; i++) {
|
||||
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) {
|
||||
return false;
|
||||
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) {
|
||||
console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
@ -574,7 +575,7 @@ module.exports = {
|
||||
const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id)
|
||||
|
||||
if (!seriesMap[librarySeries.id]) {
|
||||
const seriesObj = ctx.db.series.find(se => se.id === librarySeries.id)
|
||||
const seriesObj = Database.series.find(se => se.id === librarySeries.id)
|
||||
if (seriesObj) {
|
||||
const series = {
|
||||
...seriesObj.toJSON(),
|
||||
@ -626,7 +627,7 @@ module.exports = {
|
||||
if (libraryItem.media.metadata.authors.length) {
|
||||
for (const libraryAuthor of libraryItem.media.metadata.authors) {
|
||||
if (!authorMap[libraryAuthor.id]) {
|
||||
const authorObj = ctx.db.authors.find(au => au.id === libraryAuthor.id)
|
||||
const authorObj = Database.authors.find(au => au.id === libraryAuthor.id)
|
||||
if (authorObj) {
|
||||
const author = {
|
||||
...authorObj.toJSON(),
|
||||
|
763
server/utils/migrations/dbMigration.js
Normal file
763
server/utils/migrations/dbMigration.js
Normal file
@ -0,0 +1,763 @@
|
||||
const Path = require('path')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const Logger = require('../../Logger')
|
||||
const fs = require('../../libs/fsExtra')
|
||||
const oldDbFiles = require('./oldDbFiles')
|
||||
|
||||
const oldDbIdMap = {
|
||||
users: {},
|
||||
libraries: {},
|
||||
libraryFolders: {},
|
||||
libraryItems: {},
|
||||
authors: {},
|
||||
series: {},
|
||||
collections: {},
|
||||
podcastEpisodes: {},
|
||||
books: {}, // key is library item id
|
||||
podcasts: {}, // key is library item id
|
||||
devices: {} // key is a json stringify of the old DeviceInfo data OR deviceId if it exists
|
||||
}
|
||||
const newRecords = {
|
||||
user: [],
|
||||
library: [],
|
||||
libraryFolder: [],
|
||||
author: [],
|
||||
book: [],
|
||||
podcast: [],
|
||||
libraryItem: [],
|
||||
bookAuthor: [],
|
||||
series: [],
|
||||
bookSeries: [],
|
||||
podcastEpisode: [],
|
||||
mediaProgress: [],
|
||||
device: [],
|
||||
playbackSession: [],
|
||||
collection: [],
|
||||
collectionBook: [],
|
||||
playlist: [],
|
||||
playlistMediaItem: [],
|
||||
feed: [],
|
||||
feedEpisode: [],
|
||||
setting: []
|
||||
}
|
||||
|
||||
function getDeviceInfoString(deviceInfo, UserId) {
|
||||
if (!deviceInfo) return null
|
||||
if (deviceInfo.deviceId) return deviceInfo.deviceId
|
||||
|
||||
const keys = [
|
||||
UserId,
|
||||
deviceInfo.browserName || null,
|
||||
deviceInfo.browserVersion || null,
|
||||
deviceInfo.osName || null,
|
||||
deviceInfo.osVersion || null,
|
||||
deviceInfo.clientVersion || null,
|
||||
deviceInfo.manufacturer || null,
|
||||
deviceInfo.model || null,
|
||||
deviceInfo.sdkVersion || null,
|
||||
deviceInfo.ipAddress || null
|
||||
].map(k => k || '')
|
||||
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
function migrateBook(oldLibraryItem, LibraryItem) {
|
||||
const oldBook = oldLibraryItem.media
|
||||
|
||||
//
|
||||
// Migrate Book
|
||||
//
|
||||
const Book = {
|
||||
id: uuidv4(),
|
||||
title: oldBook.metadata.title,
|
||||
subtitle: oldBook.metadata.subtitle,
|
||||
publishedYear: oldBook.metadata.publishedYear,
|
||||
publishedDate: oldBook.metadata.publishedDate,
|
||||
publisher: oldBook.metadata.publisher,
|
||||
description: oldBook.metadata.description,
|
||||
isbn: oldBook.metadata.isbn,
|
||||
asin: oldBook.metadata.asin,
|
||||
language: oldBook.metadata.language,
|
||||
explicit: !!oldBook.metadata.explicit,
|
||||
abridged: !!oldBook.metadata.abridged,
|
||||
lastCoverSearchQuery: oldBook.lastCoverSearchQuery,
|
||||
lastCoverSearch: oldBook.lastCoverSearch,
|
||||
createdAt: LibraryItem.createdAt,
|
||||
updatedAt: LibraryItem.updatedAt,
|
||||
narrators: oldBook.metadata.narrators,
|
||||
ebookFile: oldBook.ebookFile,
|
||||
coverPath: oldBook.coverPath,
|
||||
audioFiles: oldBook.audioFiles,
|
||||
chapters: oldBook.chapters,
|
||||
tags: oldBook.tags,
|
||||
genres: oldBook.metadata.genres
|
||||
}
|
||||
newRecords.book.push(Book)
|
||||
oldDbIdMap.books[oldLibraryItem.id] = Book.id
|
||||
|
||||
//
|
||||
// Migrate BookAuthors
|
||||
//
|
||||
for (const oldBookAuthor of oldBook.metadata.authors) {
|
||||
if (oldDbIdMap.authors[oldBookAuthor.id]) {
|
||||
newRecords.bookAuthor.push({
|
||||
id: uuidv4(),
|
||||
authorId: oldDbIdMap.authors[oldBookAuthor.id],
|
||||
bookId: Book.id
|
||||
})
|
||||
} else {
|
||||
Logger.warn(`[dbMigration] migrateBook: Book author not found "${oldBookAuthor.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Migrate BookSeries
|
||||
//
|
||||
for (const oldBookSeries of oldBook.metadata.series) {
|
||||
if (oldDbIdMap.series[oldBookSeries.id]) {
|
||||
const BookSeries = {
|
||||
id: uuidv4(),
|
||||
sequence: oldBookSeries.sequence,
|
||||
seriesId: oldDbIdMap.series[oldBookSeries.id],
|
||||
bookId: Book.id
|
||||
}
|
||||
newRecords.bookSeries.push(BookSeries)
|
||||
} else {
|
||||
Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migratePodcast(oldLibraryItem, LibraryItem) {
|
||||
const oldPodcast = oldLibraryItem.media
|
||||
const oldPodcastMetadata = oldPodcast.metadata
|
||||
|
||||
//
|
||||
// Migrate Podcast
|
||||
//
|
||||
const Podcast = {
|
||||
id: uuidv4(),
|
||||
title: oldPodcastMetadata.title,
|
||||
author: oldPodcastMetadata.author,
|
||||
releaseDate: oldPodcastMetadata.releaseDate,
|
||||
feedURL: oldPodcastMetadata.feedUrl,
|
||||
imageURL: oldPodcastMetadata.imageUrl,
|
||||
description: oldPodcastMetadata.description,
|
||||
itunesPageURL: oldPodcastMetadata.itunesPageUrl,
|
||||
itunesId: oldPodcastMetadata.itunesId,
|
||||
itunesArtistId: oldPodcastMetadata.itunesArtistId,
|
||||
language: oldPodcastMetadata.language,
|
||||
podcastType: oldPodcastMetadata.type,
|
||||
explicit: !!oldPodcastMetadata.explicit,
|
||||
autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
|
||||
lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
|
||||
maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
|
||||
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
|
||||
lastCoverSearchQuery: oldPodcast.lastCoverSearchQuery,
|
||||
lastCoverSearch: oldPodcast.lastCoverSearch,
|
||||
createdAt: LibraryItem.createdAt,
|
||||
updatedAt: LibraryItem.updatedAt,
|
||||
coverPath: oldPodcast.coverPath,
|
||||
tags: oldPodcast.tags,
|
||||
genres: oldPodcastMetadata.genres
|
||||
}
|
||||
newRecords.podcast.push(Podcast)
|
||||
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id
|
||||
|
||||
//
|
||||
// Migrate PodcastEpisodes
|
||||
//
|
||||
const oldEpisodes = oldPodcast.episodes || []
|
||||
for (const oldEpisode of oldEpisodes) {
|
||||
const PodcastEpisode = {
|
||||
id: uuidv4(),
|
||||
index: oldEpisode.index,
|
||||
season: oldEpisode.season,
|
||||
episode: oldEpisode.episode,
|
||||
episodeType: oldEpisode.episodeType,
|
||||
title: oldEpisode.title,
|
||||
subtitle: oldEpisode.subtitle,
|
||||
description: oldEpisode.description,
|
||||
pubDate: oldEpisode.pubDate,
|
||||
enclosureURL: oldEpisode.enclosure?.url || null,
|
||||
enclosureSize: oldEpisode.enclosure?.length || null,
|
||||
enclosureType: oldEpisode.enclosure?.type || null,
|
||||
publishedAt: oldEpisode.publishedAt,
|
||||
createdAt: oldEpisode.addedAt,
|
||||
updatedAt: oldEpisode.updatedAt,
|
||||
podcastId: Podcast.id,
|
||||
audioFile: oldEpisode.audioFile,
|
||||
chapters: oldEpisode.chapters
|
||||
}
|
||||
newRecords.podcastEpisode.push(PodcastEpisode)
|
||||
oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLibraryItems(oldLibraryItems) {
|
||||
for (const oldLibraryItem of oldLibraryItems) {
|
||||
const libraryFolderId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId]
|
||||
if (!libraryFolderId) {
|
||||
Logger.error(`[dbMigration] migrateLibraryItems: Old library folder id not found "${oldLibraryItem.folderId}"`)
|
||||
continue
|
||||
}
|
||||
const libraryId = oldDbIdMap.libraries[oldLibraryItem.libraryId]
|
||||
if (!libraryId) {
|
||||
Logger.error(`[dbMigration] migrateLibraryItems: Old library id not found "${oldLibraryItem.libraryId}"`)
|
||||
continue
|
||||
}
|
||||
if (!['book', 'podcast'].includes(oldLibraryItem.mediaType)) {
|
||||
Logger.error(`[dbMigration] migrateLibraryItems: Not migrating library item with mediaType=${oldLibraryItem.mediaType}`)
|
||||
continue
|
||||
}
|
||||
|
||||
//
|
||||
// Migrate LibraryItem
|
||||
//
|
||||
const LibraryItem = {
|
||||
id: uuidv4(),
|
||||
ino: oldLibraryItem.ino,
|
||||
path: oldLibraryItem.path,
|
||||
relPath: oldLibraryItem.relPath,
|
||||
mediaId: null, // set below
|
||||
mediaType: oldLibraryItem.mediaType,
|
||||
isFile: !!oldLibraryItem.isFile,
|
||||
isMissing: !!oldLibraryItem.isMissing,
|
||||
isInvalid: !!oldLibraryItem.isInvalid,
|
||||
mtime: oldLibraryItem.mtimeMs,
|
||||
ctime: oldLibraryItem.ctimeMs,
|
||||
birthtime: oldLibraryItem.birthtimeMs,
|
||||
lastScan: oldLibraryItem.lastScan,
|
||||
lastScanVersion: oldLibraryItem.scanVersion,
|
||||
createdAt: oldLibraryItem.addedAt,
|
||||
updatedAt: oldLibraryItem.updatedAt,
|
||||
libraryId,
|
||||
libraryFolderId,
|
||||
libraryFiles: oldLibraryItem.libraryFiles.map(lf => {
|
||||
if (lf.isSupplementary === undefined) lf.isSupplementary = null
|
||||
return lf
|
||||
})
|
||||
}
|
||||
oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id
|
||||
newRecords.libraryItem.push(LibraryItem)
|
||||
|
||||
//
|
||||
// Migrate Book/Podcast
|
||||
//
|
||||
if (oldLibraryItem.mediaType === 'book') {
|
||||
migrateBook(oldLibraryItem, LibraryItem)
|
||||
LibraryItem.mediaId = oldDbIdMap.books[oldLibraryItem.id]
|
||||
} else if (oldLibraryItem.mediaType === 'podcast') {
|
||||
migratePodcast(oldLibraryItem, LibraryItem)
|
||||
LibraryItem.mediaId = oldDbIdMap.podcasts[oldLibraryItem.id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrateLibraries(oldLibraries) {
|
||||
for (const oldLibrary of oldLibraries) {
|
||||
if (!['book', 'podcast'].includes(oldLibrary.mediaType)) {
|
||||
Logger.error(`[dbMigration] migrateLibraries: Not migrating library with mediaType=${oldLibrary.mediaType}`)
|
||||
continue
|
||||
}
|
||||
|
||||
//
|
||||
// Migrate Library
|
||||
//
|
||||
const Library = {
|
||||
id: uuidv4(),
|
||||
name: oldLibrary.name,
|
||||
displayOrder: oldLibrary.displayOrder,
|
||||
icon: oldLibrary.icon || null,
|
||||
mediaType: oldLibrary.mediaType || null,
|
||||
provider: oldLibrary.provider,
|
||||
settings: oldLibrary.settings || {},
|
||||
createdAt: oldLibrary.createdAt,
|
||||
updatedAt: oldLibrary.lastUpdate
|
||||
}
|
||||
oldDbIdMap.libraries[oldLibrary.id] = Library.id
|
||||
newRecords.library.push(Library)
|
||||
|
||||
//
|
||||
// Migrate LibraryFolders
|
||||
//
|
||||
for (const oldFolder of oldLibrary.folders) {
|
||||
const LibraryFolder = {
|
||||
id: uuidv4(),
|
||||
path: oldFolder.fullPath,
|
||||
createdAt: oldFolder.addedAt,
|
||||
updatedAt: oldLibrary.lastUpdate,
|
||||
libraryId: Library.id
|
||||
}
|
||||
oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id
|
||||
newRecords.libraryFolder.push(LibraryFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrateAuthors(oldAuthors) {
|
||||
for (const oldAuthor of oldAuthors) {
|
||||
const Author = {
|
||||
id: uuidv4(),
|
||||
name: oldAuthor.name,
|
||||
asin: oldAuthor.asin || null,
|
||||
description: oldAuthor.description,
|
||||
imagePath: oldAuthor.imagePath,
|
||||
createdAt: oldAuthor.addedAt || Date.now(),
|
||||
updatedAt: oldAuthor.updatedAt || Date.now()
|
||||
}
|
||||
oldDbIdMap.authors[oldAuthor.id] = Author.id
|
||||
newRecords.author.push(Author)
|
||||
}
|
||||
}
|
||||
|
||||
function migrateSeries(oldSerieses) {
|
||||
for (const oldSeries of oldSerieses) {
|
||||
const Series = {
|
||||
id: uuidv4(),
|
||||
name: oldSeries.name,
|
||||
description: oldSeries.description || null,
|
||||
createdAt: oldSeries.addedAt || Date.now(),
|
||||
updatedAt: oldSeries.updatedAt || Date.now()
|
||||
}
|
||||
oldDbIdMap.series[oldSeries.id] = Series.id
|
||||
newRecords.series.push(Series)
|
||||
}
|
||||
}
|
||||
|
||||
function migrateUsers(oldUsers) {
|
||||
for (const oldUser of oldUsers) {
|
||||
//
|
||||
// Migrate User
|
||||
//
|
||||
const User = {
|
||||
id: uuidv4(),
|
||||
username: oldUser.username,
|
||||
pash: oldUser.pash || null,
|
||||
type: oldUser.type || null,
|
||||
token: oldUser.token || null,
|
||||
isActive: !!oldUser.isActive,
|
||||
lastSeen: oldUser.lastSeen || null,
|
||||
extraData: {
|
||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
||||
oldUserId: oldUser.id // Used to keep old tokens
|
||||
},
|
||||
createdAt: oldUser.createdAt || Date.now(),
|
||||
permissions: {
|
||||
...oldUser.permissions,
|
||||
librariesAccessible: oldUser.librariesAccessible || [],
|
||||
itemTagsSelected: oldUser.itemTagsSelected || []
|
||||
},
|
||||
bookmarks: oldUser.bookmarks
|
||||
}
|
||||
oldDbIdMap.users[oldUser.id] = User.id
|
||||
newRecords.user.push(User)
|
||||
|
||||
//
|
||||
// Migrate MediaProgress
|
||||
//
|
||||
for (const oldMediaProgress of oldUser.mediaProgress) {
|
||||
let mediaItemType = 'book'
|
||||
let mediaItemId = null
|
||||
if (oldMediaProgress.episodeId) {
|
||||
mediaItemType = 'podcastEpisode'
|
||||
mediaItemId = oldDbIdMap.podcastEpisodes[oldMediaProgress.episodeId]
|
||||
} else {
|
||||
mediaItemId = oldDbIdMap.books[oldMediaProgress.libraryItemId]
|
||||
}
|
||||
|
||||
if (!mediaItemId) {
|
||||
Logger.warn(`[dbMigration] migrateUsers: Unable to find media item for media progress "${oldMediaProgress.id}"`)
|
||||
continue
|
||||
}
|
||||
|
||||
const MediaProgress = {
|
||||
id: uuidv4(),
|
||||
mediaItemId,
|
||||
mediaItemType,
|
||||
duration: oldMediaProgress.duration,
|
||||
currentTime: oldMediaProgress.currentTime,
|
||||
ebookLocation: oldMediaProgress.ebookLocation || null,
|
||||
ebookProgress: oldMediaProgress.ebookProgress || null,
|
||||
isFinished: !!oldMediaProgress.isFinished,
|
||||
hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening,
|
||||
finishedAt: oldMediaProgress.finishedAt,
|
||||
createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate,
|
||||
updatedAt: oldMediaProgress.lastUpdate,
|
||||
userId: User.id,
|
||||
extraData: {
|
||||
libraryItemId: oldDbIdMap.libraryItems[oldMediaProgress.libraryItemId],
|
||||
progress: oldMediaProgress.progress
|
||||
}
|
||||
}
|
||||
newRecords.mediaProgress.push(MediaProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrateSessions(oldSessions) {
|
||||
for (const oldSession of oldSessions) {
|
||||
const userId = oldDbIdMap.users[oldSession.userId] || null // Can be null
|
||||
|
||||
//
|
||||
// Migrate Device
|
||||
//
|
||||
let deviceId = null
|
||||
if (oldSession.deviceInfo) {
|
||||
const oldDeviceInfo = oldSession.deviceInfo
|
||||
const deviceDeviceId = getDeviceInfoString(oldDeviceInfo, userId)
|
||||
deviceId = oldDbIdMap.devices[deviceDeviceId]
|
||||
if (!deviceId) {
|
||||
let clientName = 'Unknown'
|
||||
let clientVersion = null
|
||||
let deviceName = null
|
||||
let deviceVersion = oldDeviceInfo.browserVersion || null
|
||||
let extraData = {}
|
||||
if (oldDeviceInfo.sdkVersion) {
|
||||
clientName = 'Abs Android'
|
||||
clientVersion = oldDeviceInfo.clientVersion || null
|
||||
deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}`
|
||||
deviceVersion = oldDeviceInfo.sdkVersion
|
||||
} else if (oldDeviceInfo.model) {
|
||||
clientName = 'Abs iOS'
|
||||
clientVersion = oldDeviceInfo.clientVersion || null
|
||||
deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}`
|
||||
} else if (oldDeviceInfo.osName && oldDeviceInfo.browserName) {
|
||||
clientName = 'Abs Web'
|
||||
clientVersion = oldDeviceInfo.serverVersion || null
|
||||
deviceName = `${oldDeviceInfo.osName} ${oldDeviceInfo.osVersion || 'N/A'} ${oldDeviceInfo.browserName}`
|
||||
}
|
||||
|
||||
if (oldDeviceInfo.manufacturer) {
|
||||
extraData.manufacturer = oldDeviceInfo.manufacturer
|
||||
}
|
||||
if (oldDeviceInfo.model) {
|
||||
extraData.model = oldDeviceInfo.model
|
||||
}
|
||||
if (oldDeviceInfo.osName) {
|
||||
extraData.osName = oldDeviceInfo.osName
|
||||
}
|
||||
if (oldDeviceInfo.osVersion) {
|
||||
extraData.osVersion = oldDeviceInfo.osVersion
|
||||
}
|
||||
if (oldDeviceInfo.browserName) {
|
||||
extraData.browserName = oldDeviceInfo.browserName
|
||||
}
|
||||
|
||||
const id = uuidv4()
|
||||
const Device = {
|
||||
id,
|
||||
deviceId: deviceDeviceId,
|
||||
clientName,
|
||||
clientVersion,
|
||||
ipAddress: oldDeviceInfo.ipAddress,
|
||||
deviceName, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||
deviceVersion,
|
||||
userId,
|
||||
extraData
|
||||
}
|
||||
newRecords.device.push(Device)
|
||||
oldDbIdMap.devices[deviceDeviceId] = Device.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Migrate PlaybackSession
|
||||
//
|
||||
let mediaItemId = null
|
||||
let mediaItemType = 'book'
|
||||
if (oldSession.mediaType === 'podcast') {
|
||||
mediaItemId = oldDbIdMap.podcastEpisodes[oldSession.episodeId] || null
|
||||
mediaItemType = 'podcastEpisode'
|
||||
} else {
|
||||
mediaItemId = oldDbIdMap.books[oldSession.libraryItemId] || null
|
||||
}
|
||||
|
||||
const PlaybackSession = {
|
||||
id: uuidv4(),
|
||||
mediaItemId, // Can be null
|
||||
mediaItemType,
|
||||
libraryId: oldDbIdMap.libraries[oldSession.libraryId] || null,
|
||||
displayTitle: oldSession.displayTitle,
|
||||
displayAuthor: oldSession.displayAuthor,
|
||||
duration: oldSession.duration,
|
||||
playMethod: oldSession.playMethod,
|
||||
mediaPlayer: oldSession.mediaPlayer,
|
||||
startTime: oldSession.startTime,
|
||||
currentTime: oldSession.currentTime,
|
||||
serverVersion: oldSession.deviceInfo?.serverVersion || null,
|
||||
createdAt: oldSession.startedAt,
|
||||
updatedAt: oldSession.updatedAt,
|
||||
userId, // Can be null
|
||||
deviceId,
|
||||
timeListening: oldSession.timeListening,
|
||||
coverPath: oldSession.coverPath,
|
||||
mediaMetadata: oldSession.mediaMetadata,
|
||||
date: oldSession.date,
|
||||
dayOfWeek: oldSession.dayOfWeek,
|
||||
extraData: {
|
||||
libraryItemId: oldDbIdMap.libraryItems[oldSession.libraryItemId]
|
||||
}
|
||||
}
|
||||
newRecords.playbackSession.push(PlaybackSession)
|
||||
}
|
||||
}
|
||||
|
||||
function migrateCollections(oldCollections) {
|
||||
for (const oldCollection of oldCollections) {
|
||||
const libraryId = oldDbIdMap.libraries[oldCollection.libraryId]
|
||||
if (!libraryId) {
|
||||
Logger.warn(`[dbMigration] migrateCollections: Library not found for collection "${oldCollection.name}" (id:${oldCollection.libraryId})`)
|
||||
continue
|
||||
}
|
||||
|
||||
const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid)
|
||||
if (!BookIds.length) {
|
||||
Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`)
|
||||
continue
|
||||
}
|
||||
|
||||
const Collection = {
|
||||
id: uuidv4(),
|
||||
name: oldCollection.name,
|
||||
description: oldCollection.description,
|
||||
createdAt: oldCollection.createdAt,
|
||||
updatedAt: oldCollection.lastUpdate,
|
||||
libraryId
|
||||
}
|
||||
oldDbIdMap.collections[oldCollection.id] = Collection.id
|
||||
newRecords.collection.push(Collection)
|
||||
|
||||
let order = 1
|
||||
BookIds.forEach((bookId) => {
|
||||
const CollectionBook = {
|
||||
id: uuidv4(),
|
||||
createdAt: Collection.createdAt,
|
||||
bookId,
|
||||
collectionId: Collection.id,
|
||||
order: order++
|
||||
}
|
||||
newRecords.collectionBook.push(CollectionBook)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function migratePlaylists(oldPlaylists) {
|
||||
for (const oldPlaylist of oldPlaylists) {
|
||||
const libraryId = oldDbIdMap.libraries[oldPlaylist.libraryId]
|
||||
if (!libraryId) {
|
||||
Logger.warn(`[dbMigration] migratePlaylists: Library not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.libraryId})`)
|
||||
continue
|
||||
}
|
||||
|
||||
const userId = oldDbIdMap.users[oldPlaylist.userId]
|
||||
if (!userId) {
|
||||
Logger.warn(`[dbMigration] migratePlaylists: User not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.userId})`)
|
||||
continue
|
||||
}
|
||||
|
||||
let mediaItemType = 'book'
|
||||
let MediaItemIds = []
|
||||
oldPlaylist.items.forEach((itemObj) => {
|
||||
if (itemObj.episodeId) {
|
||||
mediaItemType = 'podcastEpisode'
|
||||
if (oldDbIdMap.podcastEpisodes[itemObj.episodeId]) {
|
||||
MediaItemIds.push(oldDbIdMap.podcastEpisodes[itemObj.episodeId])
|
||||
}
|
||||
} else if (oldDbIdMap.books[itemObj.libraryItemId]) {
|
||||
MediaItemIds.push(oldDbIdMap.books[itemObj.libraryItemId])
|
||||
}
|
||||
})
|
||||
if (!MediaItemIds.length) {
|
||||
Logger.warn(`[dbMigration] migratePlaylists: Playlist "${oldPlaylist.name}" has no items`)
|
||||
continue
|
||||
}
|
||||
|
||||
const Playlist = {
|
||||
id: uuidv4(),
|
||||
name: oldPlaylist.name,
|
||||
description: oldPlaylist.description,
|
||||
createdAt: oldPlaylist.createdAt,
|
||||
updatedAt: oldPlaylist.lastUpdate,
|
||||
userId,
|
||||
libraryId
|
||||
}
|
||||
newRecords.playlist.push(Playlist)
|
||||
|
||||
let order = 1
|
||||
MediaItemIds.forEach((mediaItemId) => {
|
||||
const PlaylistMediaItem = {
|
||||
id: uuidv4(),
|
||||
mediaItemId,
|
||||
mediaItemType,
|
||||
createdAt: Playlist.createdAt,
|
||||
playlistId: Playlist.id,
|
||||
order: order++
|
||||
}
|
||||
newRecords.playlistMediaItem.push(PlaylistMediaItem)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function migrateFeeds(oldFeeds) {
|
||||
for (const oldFeed of oldFeeds) {
|
||||
if (!oldFeed.episodes?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
let entityId = null
|
||||
|
||||
if (oldFeed.entityType === 'collection') {
|
||||
entityId = oldDbIdMap.collections[oldFeed.entityId]
|
||||
} else if (oldFeed.entityType === 'libraryItem') {
|
||||
entityId = oldDbIdMap.libraryItems[oldFeed.entityId]
|
||||
} else if (oldFeed.entityType === 'series') {
|
||||
entityId = oldDbIdMap.series[oldFeed.entityId]
|
||||
}
|
||||
|
||||
if (!entityId) {
|
||||
Logger.warn(`[dbMigration] migrateFeeds: Entity not found for feed "${oldFeed.entityType}" (id:${oldFeed.entityId})`)
|
||||
continue
|
||||
}
|
||||
|
||||
const userId = oldDbIdMap.users[oldFeed.userId]
|
||||
if (!userId) {
|
||||
Logger.warn(`[dbMigration] migrateFeeds: User not found for feed (id:${oldFeed.userId})`)
|
||||
continue
|
||||
}
|
||||
|
||||
const oldFeedMeta = oldFeed.meta
|
||||
|
||||
const Feed = {
|
||||
id: uuidv4(),
|
||||
slug: oldFeed.slug,
|
||||
entityType: oldFeed.entityType,
|
||||
entityId,
|
||||
entityUpdatedAt: oldFeed.entityUpdatedAt,
|
||||
serverAddress: oldFeed.serverAddress,
|
||||
feedURL: oldFeed.feedUrl,
|
||||
imageURL: oldFeedMeta.imageUrl,
|
||||
siteURL: oldFeedMeta.link,
|
||||
title: oldFeedMeta.title,
|
||||
description: oldFeedMeta.description,
|
||||
author: oldFeedMeta.author,
|
||||
podcastType: oldFeedMeta.type || null,
|
||||
language: oldFeedMeta.language || null,
|
||||
ownerName: oldFeedMeta.ownerName || null,
|
||||
ownerEmail: oldFeedMeta.ownerEmail || null,
|
||||
explicit: !!oldFeedMeta.explicit,
|
||||
preventIndexing: !!oldFeedMeta.preventIndexing,
|
||||
createdAt: oldFeed.createdAt,
|
||||
updatedAt: oldFeed.updatedAt,
|
||||
userId
|
||||
}
|
||||
newRecords.feed.push(Feed)
|
||||
|
||||
//
|
||||
// Migrate FeedEpisodes
|
||||
//
|
||||
for (const oldFeedEpisode of oldFeed.episodes) {
|
||||
const FeedEpisode = {
|
||||
id: uuidv4(),
|
||||
title: oldFeedEpisode.title,
|
||||
author: oldFeedEpisode.author,
|
||||
description: oldFeedEpisode.description,
|
||||
siteURL: oldFeedEpisode.link,
|
||||
enclosureURL: oldFeedEpisode.enclosure?.url || null,
|
||||
enclosureType: oldFeedEpisode.enclosure?.type || null,
|
||||
enclosureSize: oldFeedEpisode.enclosure?.size || null,
|
||||
pubDate: oldFeedEpisode.pubDate,
|
||||
season: oldFeedEpisode.season || null,
|
||||
episode: oldFeedEpisode.episode || null,
|
||||
episodeType: oldFeedEpisode.episodeType || null,
|
||||
duration: oldFeedEpisode.duration,
|
||||
filePath: oldFeedEpisode.fullPath,
|
||||
explicit: !!oldFeedEpisode.explicit,
|
||||
createdAt: oldFeed.createdAt,
|
||||
updatedAt: oldFeed.updatedAt,
|
||||
feedId: Feed.id
|
||||
}
|
||||
newRecords.feedEpisode.push(FeedEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrateSettings(oldSettings) {
|
||||
const serverSettings = oldSettings.find(s => s.id === 'server-settings')
|
||||
const notificationSettings = oldSettings.find(s => s.id === 'notification-settings')
|
||||
const emailSettings = oldSettings.find(s => s.id === 'email-settings')
|
||||
|
||||
if (serverSettings) {
|
||||
newRecords.setting.push({
|
||||
key: 'server-settings',
|
||||
value: serverSettings
|
||||
})
|
||||
}
|
||||
|
||||
if (notificationSettings) {
|
||||
newRecords.setting.push({
|
||||
key: 'notification-settings',
|
||||
value: notificationSettings
|
||||
})
|
||||
}
|
||||
|
||||
if (emailSettings) {
|
||||
newRecords.setting.push({
|
||||
key: 'email-settings',
|
||||
value: emailSettings
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.migrate = async (DatabaseModels) => {
|
||||
Logger.info(`[dbMigration] Starting migration`)
|
||||
|
||||
const data = await oldDbFiles.init()
|
||||
|
||||
const start = Date.now()
|
||||
migrateSettings(data.settings)
|
||||
migrateAuthors(data.authors)
|
||||
migrateSeries(data.series)
|
||||
migrateLibraries(data.libraries)
|
||||
migrateLibraryItems(data.libraryItems)
|
||||
migrateUsers(data.users)
|
||||
migrateSessions(data.sessions)
|
||||
migrateCollections(data.collections)
|
||||
migratePlaylists(data.playlists)
|
||||
migrateFeeds(data.feeds)
|
||||
|
||||
let totalRecords = 0
|
||||
for (const model in newRecords) {
|
||||
Logger.info(`[dbMigration] Inserting ${newRecords[model].length} ${model} rows`)
|
||||
if (newRecords[model].length) {
|
||||
await DatabaseModels[model].bulkCreate(newRecords[model])
|
||||
totalRecords += newRecords[model].length
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - start
|
||||
|
||||
// Purge author images and cover images from cache
|
||||
try {
|
||||
const CachePath = Path.join(global.MetadataPath, 'cache')
|
||||
await fs.emptyDir(Path.join(CachePath, 'covers'))
|
||||
await fs.emptyDir(Path.join(CachePath, 'images'))
|
||||
} catch (error) {
|
||||
Logger.error(`[dbMigration] Failed to purge author/cover image cache`, error)
|
||||
}
|
||||
|
||||
// Put all old db folders into a zipfile oldDb.zip
|
||||
await oldDbFiles.zipWrapOldDb()
|
||||
|
||||
Logger.info(`[dbMigration] Migration complete. ${totalRecords} rows. Elapsed ${(elapsed / 1000).toFixed(2)}s`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if old database exists
|
||||
*/
|
||||
module.exports.checkShouldMigrate = async (force = false) => {
|
||||
if (await oldDbFiles.checkHasOldDb()) return true
|
||||
if (!force) return false
|
||||
return oldDbFiles.checkHasOldDbZip()
|
||||
}
|
189
server/utils/migrations/oldDbFiles.js
Normal file
189
server/utils/migrations/oldDbFiles.js
Normal file
@ -0,0 +1,189 @@
|
||||
const { once } = require('events')
|
||||
const { createInterface } = require('readline')
|
||||
const Path = require('path')
|
||||
const Logger = require('../../Logger')
|
||||
const fs = require('../../libs/fsExtra')
|
||||
const archiver = require('../../libs/archiver')
|
||||
const StreamZip = require('../../libs/nodeStreamZip')
|
||||
|
||||
async function processDbFile(filepath) {
|
||||
if (!fs.pathExistsSync(filepath)) {
|
||||
Logger.error(`[oldDbFiles] Db file does not exist at "${filepath}"`)
|
||||
return []
|
||||
}
|
||||
|
||||
const entities = []
|
||||
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filepath)
|
||||
|
||||
const rl = createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
})
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (line && line.trim()) {
|
||||
try {
|
||||
const entity = JSON.parse(line)
|
||||
if (entity && Object.keys(entity).length) entities.push(entity)
|
||||
} catch (jsonParseError) {
|
||||
Logger.error(`[oldDbFiles] Failed to parse line "${line}" in db file "${filepath}"`, jsonParseError)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await once(rl, 'close')
|
||||
|
||||
console.log(`[oldDbFiles] Db file "${filepath}" processed`)
|
||||
|
||||
return entities
|
||||
} catch (error) {
|
||||
Logger.error(`[oldDbFiles] Failed to read db file "${filepath}"`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDbData(dbpath) {
|
||||
try {
|
||||
Logger.info(`[oldDbFiles] Loading db data at "${dbpath}"`)
|
||||
const files = await fs.readdir(dbpath)
|
||||
|
||||
const entities = []
|
||||
for (const filename of files) {
|
||||
if (Path.extname(filename).toLowerCase() !== '.json') {
|
||||
Logger.warn(`[oldDbFiles] Ignoring filename "${filename}" in db folder "${dbpath}"`)
|
||||
continue
|
||||
}
|
||||
|
||||
const filepath = Path.join(dbpath, filename)
|
||||
Logger.info(`[oldDbFiles] Loading db data file "${filepath}"`)
|
||||
const someEntities = await processDbFile(filepath)
|
||||
Logger.info(`[oldDbFiles] Processed db data file with ${someEntities.length} entities`)
|
||||
entities.push(...someEntities)
|
||||
}
|
||||
|
||||
Logger.info(`[oldDbFiles] Finished loading db data with ${entities.length} entities`)
|
||||
return entities
|
||||
} catch (error) {
|
||||
Logger.error(`[oldDbFiles] Failed to load db data "${dbpath}"`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.init = async () => {
|
||||
const dbs = {
|
||||
libraryItems: Path.join(global.ConfigPath, 'libraryItems', 'data'),
|
||||
users: Path.join(global.ConfigPath, 'users', 'data'),
|
||||
sessions: Path.join(global.ConfigPath, 'sessions', 'data'),
|
||||
libraries: Path.join(global.ConfigPath, 'libraries', 'data'),
|
||||
settings: Path.join(global.ConfigPath, 'settings', 'data'),
|
||||
collections: Path.join(global.ConfigPath, 'collections', 'data'),
|
||||
playlists: Path.join(global.ConfigPath, 'playlists', 'data'),
|
||||
authors: Path.join(global.ConfigPath, 'authors', 'data'),
|
||||
series: Path.join(global.ConfigPath, 'series', 'data'),
|
||||
feeds: Path.join(global.ConfigPath, 'feeds', 'data')
|
||||
}
|
||||
|
||||
const data = {}
|
||||
for (const key in dbs) {
|
||||
data[key] = await loadDbData(dbs[key])
|
||||
Logger.info(`[oldDbFiles] ${data[key].length} ${key} loaded`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
module.exports.zipWrapOldDb = async () => {
|
||||
const dbs = {
|
||||
libraryItems: Path.join(global.ConfigPath, 'libraryItems'),
|
||||
users: Path.join(global.ConfigPath, 'users'),
|
||||
sessions: Path.join(global.ConfigPath, 'sessions'),
|
||||
libraries: Path.join(global.ConfigPath, 'libraries'),
|
||||
settings: Path.join(global.ConfigPath, 'settings'),
|
||||
collections: Path.join(global.ConfigPath, 'collections'),
|
||||
playlists: Path.join(global.ConfigPath, 'playlists'),
|
||||
authors: Path.join(global.ConfigPath, 'authors'),
|
||||
series: Path.join(global.ConfigPath, 'series'),
|
||||
feeds: Path.join(global.ConfigPath, 'feeds')
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||
const output = fs.createWriteStream(oldDbPath)
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // Sets the compression level.
|
||||
})
|
||||
|
||||
// listen for all archive data to be written
|
||||
// 'close' event is fired only when a file descriptor is involved
|
||||
output.on('close', async () => {
|
||||
Logger.info(`[oldDbFiles] Old db files have been zipped in ${oldDbPath}. ${archive.pointer()} total bytes`)
|
||||
|
||||
// Remove old db folders have successful zip
|
||||
for (const db in dbs) {
|
||||
await fs.remove(dbs[db])
|
||||
}
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
// This event is fired when the data source is drained no matter what was the data source.
|
||||
// It is not part of this library but rather from the NodeJS Stream API.
|
||||
// @see: https://nodejs.org/api/stream.html#stream_event_end
|
||||
output.on('end', () => {
|
||||
Logger.debug('[oldDbFiles] Data has been drained')
|
||||
})
|
||||
|
||||
// good practice to catch this error explicitly
|
||||
archive.on('error', (err) => {
|
||||
Logger.error(`[oldDbFiles] Failed to zip old db folders`, err)
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
// pipe archive data to the file
|
||||
archive.pipe(output)
|
||||
|
||||
for (const db in dbs) {
|
||||
archive.directory(dbs[db], db)
|
||||
}
|
||||
|
||||
// finalize the archive (ie we are done appending files but streams have to finish yet)
|
||||
// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
|
||||
archive.finalize()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.checkHasOldDb = async () => {
|
||||
const dbs = {
|
||||
libraryItems: Path.join(global.ConfigPath, 'libraryItems'),
|
||||
users: Path.join(global.ConfigPath, 'users'),
|
||||
sessions: Path.join(global.ConfigPath, 'sessions'),
|
||||
libraries: Path.join(global.ConfigPath, 'libraries'),
|
||||
settings: Path.join(global.ConfigPath, 'settings'),
|
||||
collections: Path.join(global.ConfigPath, 'collections'),
|
||||
playlists: Path.join(global.ConfigPath, 'playlists'),
|
||||
authors: Path.join(global.ConfigPath, 'authors'),
|
||||
series: Path.join(global.ConfigPath, 'series'),
|
||||
feeds: Path.join(global.ConfigPath, 'feeds')
|
||||
}
|
||||
for (const db in dbs) {
|
||||
if (await fs.pathExists(dbs[db])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports.checkHasOldDbZip = async () => {
|
||||
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||
if (!await fs.pathExists(oldDbPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract oldDb.zip
|
||||
const zip = new StreamZip.async({ file: oldDbPath })
|
||||
await zip.extract(null, global.ConfigPath)
|
||||
|
||||
return this.checkHasOldDb()
|
||||
}
|
Loading…
Reference in New Issue
Block a user