diff --git a/client/components/modals/item/tabs/Episodes.vue b/client/components/modals/item/tabs/Episodes.vue index f64eea4e..661f41e0 100644 --- a/client/components/modals/item/tabs/Episodes.vue +++ b/client/components/modals/item/tabs/Episodes.vue @@ -20,18 +20,14 @@
Sort # | -{{ $strings.LabelEpisode }} | -{{ $strings.EpisodeTitle }} | -{{ $strings.EpisodeDuration }} | -{{ $strings.EpisodeSize }} | +{{ $strings.LabelEpisode }} | +{{ $strings.LabelEpisodeTitle }} | +{{ $strings.LabelEpisodeDuration }} | +{{ $strings.LabelEpisodeSize }} |
---|---|---|---|---|---|---|---|---|
- {{ episode.index }} - |
-
- {{ episode.episode }} + |
+ {{ episode.episode }} |
{{ episode.title }}
diff --git a/server/Auth.js b/server/Auth.js
index c9d67b20..37ea4bb1 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -32,12 +32,13 @@ class Auth {
await Database.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user
- if (Database.users.length) {
- for (const user of Database.users) {
+ const users = await Database.models.user.getOldUsers()
+ if (users.length) {
+ for (const user of 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 Database.updateBulkUsers(Database.users)
+ await Database.updateBulkUsers(users)
}
}
@@ -93,21 +94,32 @@ class Auth {
verifyToken(token) {
return new Promise((resolve) => {
- jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
+ jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => {
if (!payload || err) {
Logger.error('JWT Verify Token Failed', err)
return resolve(null)
}
- const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username)
- resolve(user || null)
+
+ const user = await Database.models.user.getUserByIdOrOldId(payload.userId)
+ if (user && user.username === payload.username) {
+ resolve(user)
+ } else {
+ resolve(null)
+ }
})
})
}
- getUserLoginResponsePayload(user) {
+ /**
+ * Payload returned to a user after successful login
+ * @param {oldUser} user
+ * @returns {object}
+ */
+ async getUserLoginResponsePayload(user) {
+ const libraryIds = await Database.models.library.getAllLibraryIds()
return {
user: user.toJSONForBrowser(),
- userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
+ userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
serverSettings: Database.serverSettings.toJSONForBrowser(),
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
Source: global.Source
@@ -119,7 +131,7 @@ class Auth {
const username = (req.body.username || '').toLowerCase()
const password = req.body.password || ''
- const user = Database.users.find(u => u.username.toLowerCase() === username)
+ const user = await Database.models.user.getUserByUsername(username)
if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
@@ -136,7 +148,8 @@ class Auth {
return res.status(401).send('Invalid root password (hint: there is none)')
} else {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
- return res.json(this.getUserLoginResponsePayload(user))
+ const userLoginResponsePayload = await this.getUserLoginResponsePayload(user)
+ return res.json(userLoginResponsePayload)
}
}
@@ -144,7 +157,8 @@ class Auth {
const compare = await bcrypt.compare(password, user.pash)
if (compare) {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
- res.json(this.getUserLoginResponsePayload(user))
+ const userLoginResponsePayload = await this.getUserLoginResponsePayload(user)
+ res.json(userLoginResponsePayload)
} else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) {
@@ -164,7 +178,7 @@ class Auth {
async userChangePassword(req, res) {
var { password, newPassword } = req.body
newPassword = newPassword || ''
- const matchingUser = Database.users.find(u => u.id === req.user.id)
+ const matchingUser = await Database.models.user.getUserById(req.user.id)
// Only root can have an empty password
if (matchingUser.type !== 'root' && !newPassword) {
diff --git a/server/Database.js b/server/Database.js
index 5e230161..65b85070 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -6,21 +6,19 @@ const fs = require('./libs/fsExtra')
const Logger = require('./Logger')
const dbMigration = require('./utils/migrations/dbMigration')
+const Auth = require('./Auth')
class Database {
constructor() {
this.sequelize = null
this.dbPath = null
this.isNew = false // New absdatabase.sqlite created
+ this.hasRootUser = false // Used to show initialization page in web ui
// Temporarily using format of old DB
// TODO: 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 = []
@@ -33,10 +31,6 @@ class Database {
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] absdatabase.sqlite not found at ${this.dbPath}`)
@@ -66,7 +60,8 @@ class Database {
this.sequelize = new Sequelize({
dialect: 'sqlite',
storage: this.dbPath,
- logging: false
+ logging: false,
+ transactionType: 'IMMEDIATE'
})
// Helper function
@@ -164,24 +159,15 @@ class Database {
this.libraryItems = await this.models.libraryItem.loadAllLibraryItems()
Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`)
- this.users = await this.models.user.getOldUsers()
- Logger.info(`[Database] Loaded ${this.users.length} users`)
-
- this.libraries = await this.models.library.getAllOldLibraries()
- Logger.info(`[Database] Loaded ${this.libraries.length} libraries`)
-
- this.collections = await this.models.collection.getOldCollections()
- Logger.info(`[Database] Loaded ${this.collections.length} collections`)
-
- this.playlists = await this.models.playlist.getOldPlaylists()
- Logger.info(`[Database] Loaded ${this.playlists.length} playlists`)
-
this.authors = await this.models.author.getOldAuthors()
Logger.info(`[Database] Loaded ${this.authors.length} authors`)
this.series = await this.models.series.getAllOldSeries()
Logger.info(`[Database] Loaded ${this.series.length} series`)
+ // Set if root user has been created
+ this.hasRootUser = await this.models.user.getHasRootUser()
+
Logger.info(`[Database] Db data loaded in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
if (packageJson.version !== this.serverSettings.version) {
@@ -191,14 +177,18 @@ class Database {
}
}
- async createRootUser(username, pash, token) {
+ /**
+ * Create root user
+ * @param {string} username
+ * @param {string} pash
+ * @param {Auth} auth
+ * @returns {boolean} true if created
+ */
+ async createRootUser(username, pash, auth) {
if (!this.sequelize) return false
- const newUser = await this.models.user.createRootUser(username, pash, token)
- if (newUser) {
- this.users.push(newUser)
- return true
- }
- return false
+ await this.models.user.createRootUser(username, pash, auth)
+ this.hasRootUser = true
+ return true
}
updateServerSettings() {
@@ -215,7 +205,6 @@ class Database {
async createUser(oldUser) {
if (!this.sequelize) return false
await this.models.user.createFromOld(oldUser)
- this.users.push(oldUser)
return true
}
@@ -232,7 +221,6 @@ class Database {
async removeUser(userId) {
if (!this.sequelize) return false
await this.models.user.removeById(userId)
- this.users = this.users.filter(u => u.id !== userId)
}
upsertMediaProgress(oldMediaProgress) {
@@ -253,7 +241,6 @@ class Database {
async createLibrary(oldLibrary) {
if (!this.sequelize) return false
await this.models.library.createFromOld(oldLibrary)
- this.libraries.push(oldLibrary)
}
updateLibrary(oldLibrary) {
@@ -264,7 +251,6 @@ class Database {
async removeLibrary(libraryId) {
if (!this.sequelize) return false
await this.models.library.removeById(libraryId)
- this.libraries = this.libraries.filter(lib => lib.id !== libraryId)
}
async createCollection(oldCollection) {
@@ -286,7 +272,6 @@ class Database {
await this.createBulkCollectionBooks(collectionBooks)
}
}
- this.collections.push(oldCollection)
}
updateCollection(oldCollection) {
@@ -308,7 +293,6 @@ class Database {
async removeCollection(collectionId) {
if (!this.sequelize) return false
await this.models.collection.removeById(collectionId)
- this.collections = this.collections.filter(c => c.id !== collectionId)
}
createCollectionBook(collectionBook) {
@@ -353,7 +337,6 @@ class Database {
await this.createBulkPlaylistMediaItems(playlistMediaItems)
}
}
- this.playlists.push(oldPlaylist)
}
updatePlaylist(oldPlaylist) {
@@ -376,7 +359,6 @@ class Database {
async removePlaylist(playlistId) {
if (!this.sequelize) return false
await this.models.playlist.removeById(playlistId)
- this.playlists = this.playlists.filter(p => p.id !== playlistId)
}
createPlaylistMediaItem(playlistMediaItem) {
@@ -405,12 +387,14 @@ class Database {
async createLibraryItem(oldLibraryItem) {
if (!this.sequelize) return false
+ await oldLibraryItem.saveMetadata()
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
this.libraryItems.push(oldLibraryItem)
}
- updateLibraryItem(oldLibraryItem) {
+ async updateLibraryItem(oldLibraryItem) {
if (!this.sequelize) return false
+ await oldLibraryItem.saveMetadata()
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
}
@@ -418,8 +402,11 @@ class Database {
if (!this.sequelize) return false
let updatesMade = 0
for (const oldLibraryItem of oldLibraryItems) {
+ await oldLibraryItem.saveMetadata()
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
- if (hasUpdates) updatesMade++
+ if (hasUpdates) {
+ updatesMade++
+ }
}
return updatesMade
}
@@ -427,6 +414,7 @@ class Database {
async createBulkLibraryItems(oldLibraryItems) {
if (!this.sequelize) return false
for (const oldLibraryItem of oldLibraryItems) {
+ await oldLibraryItem.saveMetadata()
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
this.libraryItems.push(oldLibraryItem)
}
diff --git a/server/Server.js b/server/Server.js
index 38a01022..432c1b3f 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -93,6 +93,10 @@ class Server {
this.auth.authMiddleware(req, res, next)
}
+ /**
+ * Initialize database, backups, logs, rss feeds, cron jobs & watcher
+ * Cleanup stale/invalid data
+ */
async init() {
Logger.info('[Server] Init v' + version)
await this.playbackSessionManager.removeOrphanStreams()
@@ -105,20 +109,21 @@ class Server {
}
await this.cleanUserData() // Remove invalid user item progress
- await this.purgeMetadata() // Remove metadata folders without library item
await this.cacheManager.ensureCachePaths()
await this.backupManager.init()
await this.logManager.init()
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
await this.rssFeedManager.init()
- this.cronManager.init()
+
+ const libraries = await Database.models.library.getAllOldLibraries()
+ this.cronManager.init(libraries)
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
this.watcher.disabled = true
} else {
- this.watcher.initWatcher(Database.libraries)
+ this.watcher.initWatcher(libraries)
this.watcher.on('files', this.filesChanged.bind(this))
}
}
@@ -243,39 +248,10 @@ class Server {
await this.scanner.scanFilesChanged(fileUpdates)
}
- // Remove unused /metadata/items/{id} folders
- async purgeMetadata() {
- const itemsMetadata = Path.join(global.MetadataPath, 'items')
- if (!(await fs.pathExists(itemsMetadata))) return
- const foldersInItemsMetadata = await fs.readdir(itemsMetadata)
-
- let purged = 0
- await Promise.all(foldersInItemsMetadata.map(async foldername => {
- const itemFullPath = fileUtils.filePathToPOSIX(Path.join(itemsMetadata, foldername))
-
- const hasMatchingItem = Database.libraryItems.find(li => {
- if (!li.media.coverPath) return false
- return itemFullPath === fileUtils.filePathToPOSIX(Path.dirname(li.media.coverPath))
- })
- if (!hasMatchingItem) {
- Logger.debug(`[Server] Purging unused metadata ${itemFullPath}`)
-
- await fs.remove(itemFullPath).then(() => {
- purged++
- }).catch((err) => {
- Logger.error(`[Server] Failed to delete folder path ${itemFullPath}`, err)
- })
- }
- }))
- if (purged > 0) {
- Logger.info(`[Server] Purged ${purged} unused library item metadata`)
- }
- return purged
- }
-
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
async cleanUserData() {
- for (const _user of Database.users) {
+ const users = await Database.models.user.getOldUsers()
+ for (const _user of users) {
if (_user.mediaProgress.length) {
for (const mediaProgress of _user.mediaProgress) {
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js
index 4665869e..682088cc 100644
--- a/server/controllers/CollectionController.js
+++ b/server/controllers/CollectionController.js
@@ -20,9 +20,10 @@ class CollectionController {
res.json(jsonExpanded)
}
- findAll(req, res) {
+ async findAll(req, res) {
+ const collections = await Database.models.collection.getOldCollections()
res.json({
- collections: Database.collections.map(c => c.toJSONExpanded(Database.libraryItems))
+ collections: collections.map(c => c.toJSONExpanded(Database.libraryItems))
})
}
@@ -160,9 +161,9 @@ class CollectionController {
res.json(collection.toJSONExpanded(Database.libraryItems))
}
- middleware(req, res, next) {
+ async middleware(req, res, next) {
if (req.params.id) {
- const collection = Database.collections.find(c => c.id === req.params.id)
+ const collection = await Database.models.collection.getById(req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js
index edf9b736..8d538489 100644
--- a/server/controllers/FileSystemController.js
+++ b/server/controllers/FileSystemController.js
@@ -17,12 +17,11 @@ class FileSystemController {
})
// Do not include existing mapped library paths in response
- Database.libraries.forEach(lib => {
- lib.folders.forEach((folder) => {
- let dir = folder.fullPath
- if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
- excludedDirs.push(dir)
- })
+ const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
+ libraryFoldersPaths.forEach((path) => {
+ let dir = path || ''
+ if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
+ excludedDirs.push(dir)
})
res.json({
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 893bb5c7..55d0533b 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -44,7 +44,9 @@ class LibraryController {
const library = new Library()
- newLibraryPayload.displayOrder = Database.libraries.map(li => li.displayOrder).sort((a, b) => a - b).pop() + 1
+ let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder()
+ if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
+ newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
library.setData(newLibraryPayload)
await Database.createLibrary(library)
@@ -60,17 +62,18 @@ class LibraryController {
res.json(library)
}
- findAll(req, res) {
+ async findAll(req, res) {
+ const libraries = await Database.models.library.getAllOldLibraries()
+
const librariesAccessible = req.user.librariesAccessible || []
if (librariesAccessible.length) {
return res.json({
- libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
+ libraries: libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
})
}
res.json({
- libraries: Database.libraries.map(lib => lib.toJSON())
- // libraries: Database.libraries.map(lib => lib.toJSON())
+ libraries: libraries.map(lib => lib.toJSON())
})
}
@@ -80,7 +83,7 @@ class LibraryController {
return res.json({
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
issues: req.libraryItems.filter(li => li.hasIssues).length,
- numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
+ numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
library: req.library
})
}
@@ -151,6 +154,12 @@ class LibraryController {
return res.json(library.toJSON())
}
+ /**
+ * DELETE: /api/libraries/:id
+ * Delete a library
+ * @param {*} req
+ * @param {*} res
+ */
async delete(req, res) {
const library = req.library
@@ -158,10 +167,9 @@ class LibraryController {
this.watcher.removeLibrary(library)
// Remove collections for library
- 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 Database.removeCollection(collection.id)
+ const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id)
+ if (numCollectionsRemoved) {
+ Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
}
// Remove items in this library
@@ -173,6 +181,10 @@ class LibraryController {
const libraryJson = library.toJSON()
await Database.removeLibrary(library.id)
+
+ // Re-order libraries
+ await Database.models.library.resetDisplayOrder()
+
SocketAuthority.emitter('library_removed', libraryJson)
return res.json(libraryJson)
}
@@ -514,7 +526,9 @@ class LibraryController {
include: include.join(',')
}
- let collections = await Promise.all(Database.collections.filter(c => c.libraryId === req.library.id).map(async c => {
+ const collectionsForLibrary = await Database.models.collection.getAllForLibrary(req.library.id)
+
+ let collections = await Promise.all(collectionsForLibrary.map(async c => {
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
// If all books restricted to user in this collection then hide this collection
@@ -543,7 +557,8 @@ class LibraryController {
// api/libraries/:id/playlists
async getUserPlaylistsForLibrary(req, res) {
- let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems))
+ let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
+ playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
const payload = {
results: [],
@@ -601,17 +616,23 @@ class LibraryController {
res.json(categories)
}
- // PATCH: Change the order of libraries
+ /**
+ * POST: /api/libraries/order
+ * Change the display order of libraries
+ * @param {*} req
+ * @param {*} res
+ */
async reorder(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
return res.sendStatus(403)
}
+ const libraries = await Database.models.library.getAllOldLibraries()
- var orderdata = req.body
- var hasUpdates = false
+ const orderdata = req.body
+ let hasUpdates = false
for (let i = 0; i < orderdata.length; i++) {
- var library = Database.libraries.find(lib => lib.id === orderdata[i].id)
+ const library = 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)
@@ -623,14 +644,14 @@ class LibraryController {
}
if (hasUpdates) {
- Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
+ 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: Database.libraries.map(lib => lib.toJSON())
+ libraries: libraries.map(lib => lib.toJSON())
})
}
@@ -902,13 +923,13 @@ class LibraryController {
res.send(opmlText)
}
- middleware(req, res, next) {
+ async middleware(req, res, next) {
if (!req.user.checkCanAccessLibrary(req.params.id)) {
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
return res.sendStatus(403)
}
- const library = Database.libraries.find(lib => lib.id === req.params.id)
+ const library = await Database.models.library.getOldById(req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index 0ed7ef8f..6bf889fb 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -24,18 +24,18 @@ class MiscController {
Logger.error('Invalid request, no files')
return res.sendStatus(400)
}
- var files = Object.values(req.files)
- var title = req.body.title
- var author = req.body.author
- var series = req.body.series
- var libraryId = req.body.library
- var folderId = req.body.folder
+ const files = Object.values(req.files)
+ const title = req.body.title
+ const author = req.body.author
+ const series = req.body.series
+ const libraryId = req.body.library
+ const folderId = req.body.folder
- var library = Database.libraries.find(lib => lib.id === libraryId)
+ const library = await Database.models.library.getOldById(libraryId)
if (!library) {
return res.status(404).send(`Library not found with id ${libraryId}`)
}
- var folder = library.folders.find(fold => fold.id === folderId)
+ const folder = library.folders.find(fold => fold.id === folderId)
if (!folder) {
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
}
@@ -45,8 +45,8 @@ class MiscController {
}
// For setting permissions recursively
- var outputDirectory = ''
- var firstDirPath = ''
+ let outputDirectory = ''
+ let firstDirPath = ''
if (library.isPodcast) { // Podcasts only in 1 folder
outputDirectory = Path.join(folder.fullPath, title)
@@ -62,8 +62,7 @@ class MiscController {
}
}
- var exists = await fs.pathExists(outputDirectory)
- if (exists) {
+ if (await fs.pathExists(outputDirectory)) {
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
return res.status(500).send(`Directory "${outputDirectory}" already exists`)
}
@@ -132,12 +131,19 @@ class MiscController {
})
}
- authorize(req, res) {
+ /**
+ * POST: /api/authorize
+ * Used to authorize an API token
+ *
+ * @param {*} req
+ * @param {*} res
+ */
+ async authorize(req, res) {
if (!req.user) {
Logger.error('Invalid user in authorize')
return res.sendStatus(401)
}
- const userResponse = this.auth.getUserLoginResponsePayload(req.user)
+ const userResponse = await this.auth.getUserLoginResponsePayload(req.user)
res.json(userResponse)
}
diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js
index 92eb4f37..8c351c78 100644
--- a/server/controllers/PlaylistController.js
+++ b/server/controllers/PlaylistController.js
@@ -22,9 +22,10 @@ class PlaylistController {
}
// GET: api/playlists
- findAllForUser(req, res) {
+ async findAllForUser(req, res) {
+ const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id)
res.json({
- playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems))
+ playlists: playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
})
}
@@ -200,7 +201,7 @@ class PlaylistController {
// POST: api/playlists/collection/:collectionId
async createFromCollection(req, res) {
- let collection = Database.collections.find(c => c.id === req.params.collectionId)
+ let collection = await Database.models.collection.getById(req.params.collectionId)
if (!collection) {
return res.status(404).send('Collection not found')
}
@@ -231,9 +232,9 @@ class PlaylistController {
res.json(jsonExpanded)
}
- middleware(req, res, next) {
+ async middleware(req, res, next) {
if (req.params.id) {
- const playlist = Database.playlists.find(p => p.id === req.params.id)
+ const playlist = await Database.models.playlist.getById(req.params.id)
if (!playlist) {
return res.status(404).send('Playlist not found')
}
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index fbcf007f..1d1da4f7 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -19,7 +19,7 @@ class PodcastController {
}
const payload = req.body
- const library = Database.libraries.find(lib => lib.id === payload.libraryId)
+ const library = await Database.models.library.getOldById(payload.libraryId)
if (!library) {
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
return res.status(404).send('Library not found')
@@ -241,18 +241,18 @@ class PodcastController {
// DELETE: api/podcasts/:id/episode/:episodeId
async removeEpisode(req, res) {
- var episodeId = req.params.episodeId
- var libraryItem = req.libraryItem
- var hardDelete = req.query.hard === '1'
+ const episodeId = req.params.episodeId
+ const libraryItem = req.libraryItem
+ const hardDelete = req.query.hard === '1'
- var episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
+ const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
if (!episode) {
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
return res.sendStatus(404)
}
if (hardDelete) {
- var audioFile = episode.audioFile
+ const audioFile = episode.audioFile
// TODO: this will trigger the watcher. should maybe handle this gracefully
await fs.remove(audioFile.metadata.path).then(() => {
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
@@ -267,6 +267,22 @@ class PodcastController {
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
}
+ // Update/remove playlists that had this podcast episode
+ const playlistsWithEpisode = await Database.models.playlist.getPlaylistsForMediaItemIds([episodeId])
+ for (const playlist of playlistsWithEpisode) {
+ playlist.removeItem(libraryItem.id, episodeId)
+
+ // If playlist is now empty then remove it
+ if (!playlist.items.length) {
+ Logger.info(`[PodcastController] Playlist "${playlist.name}" has no more items - removing it`)
+ await Database.removePlaylist(playlist.id)
+ SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems))
+ } else {
+ await Database.updatePlaylist(playlist)
+ SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
+ }
+ }
+
await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSON())
diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js
index a6e6abc8..82175e4c 100644
--- a/server/controllers/RSSFeedController.js
+++ b/server/controllers/RSSFeedController.js
@@ -45,7 +45,7 @@ class RSSFeedController {
async openRSSFeedForCollection(req, res) {
const options = req.body || {}
- const collection = Database.collections.find(li => li.id === req.params.collectionId)
+ const collection = await Database.models.collection.getById(req.params.collectionId)
if (!collection) return res.sendStatus(404)
// Check request body options exist
diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js
index 92dff559..698f58d7 100644
--- a/server/controllers/SessionController.js
+++ b/server/controllers/SessionController.js
@@ -43,17 +43,17 @@ class SessionController {
res.json(payload)
}
- getOpenSessions(req, res) {
+ async getOpenSessions(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
return res.sendStatus(404)
}
+ const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
const openSessions = this.playbackSessionManager.sessions.map(se => {
- const user = Database.users.find(u => u.id === se.userId) || null
return {
...se.toJSON(),
- user: user ? { id: user.id, username: user.username } : null
+ user: minifiedUserObjects.find(u => u.id === se.userId) || null
}
})
diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js
index 5945637b..45780045 100644
--- a/server/controllers/UserController.js
+++ b/server/controllers/UserController.js
@@ -17,7 +17,8 @@ class UserController {
const includes = (req.query.include || '').split(',').map(i => i.trim())
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
- const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true))
+ const allUsers = await Database.models.user.getOldUsers()
+ const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
if (includes.includes('latestSession')) {
for (const user of users) {
@@ -31,25 +32,20 @@ class UserController {
})
}
- findOne(req, res) {
+ async findOne(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user)
return res.sendStatus(403)
}
- const user = Database.users.find(u => u.id === req.params.id)
- if (!user) {
- return res.sendStatus(404)
- }
-
- res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
+ res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
}
async create(req, res) {
- var account = req.body
+ const account = req.body
+ const username = account.username
- var username = account.username
- var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
+ const usernameExists = await Database.models.user.getUserByUsername(username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@@ -73,7 +69,7 @@ class UserController {
}
async update(req, res) {
- var user = req.reqUser
+ const user = req.reqUser
if (user.type === 'root' && !req.user.isRoot) {
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
@@ -84,7 +80,7 @@ class UserController {
var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) {
- var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
+ const usernameExists = await Database.models.user.getUserByUsername(account.username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@@ -126,7 +122,7 @@ class UserController {
// Todo: check if user is logged in and cancel streams
// Remove user playlists
- const userPlaylists = Database.playlists.filter(p => p.userId === user.id)
+ const userPlaylists = await Database.models.playlist.getPlaylistsForUserAndLibrary(user.id)
for (const playlist of userPlaylists) {
await Database.removePlaylist(playlist.id)
}
@@ -178,7 +174,7 @@ class UserController {
})
}
- middleware(req, res, next) {
+ async middleware(req, res, next) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
@@ -186,7 +182,7 @@ class UserController {
}
if (req.params.id) {
- req.reqUser = Database.users.find(u => u.id === req.params.id)
+ req.reqUser = await Database.models.user.getUserById(req.params.id)
if (!req.reqUser) {
return res.sendStatus(404)
}
diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js
index adbf87a5..b5d17cb1 100644
--- a/server/managers/CronManager.js
+++ b/server/managers/CronManager.js
@@ -13,13 +13,21 @@ class CronManager {
this.podcastCronExpressionsExecuting = []
}
- init() {
- this.initLibraryScanCrons()
+ /**
+ * Initialize library scan crons & podcast download crons
+ * @param {oldLibrary[]} libraries
+ */
+ init(libraries) {
+ this.initLibraryScanCrons(libraries)
this.initPodcastCrons()
}
- initLibraryScanCrons() {
- for (const library of Database.libraries) {
+ /**
+ * Initialize library scan crons
+ * @param {oldLibrary[]} libraries
+ */
+ initLibraryScanCrons(libraries) {
+ for (const library of libraries) {
if (library.settings.autoScanCronExpression) {
this.startCronForLibrary(library)
}
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index bd62b880..5f3ab238 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -14,15 +14,15 @@ class NotificationManager {
return notificationData
}
- onPodcastEpisodeDownloaded(libraryItem, episode) {
+ async onPodcastEpisodeDownloaded(libraryItem, episode) {
if (!Database.notificationSettings.isUseable) return
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
- const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
+ const library = await Database.models.library.getOldById(libraryItem.libraryId)
const eventData = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
- libraryName: library ? library.name : 'Unknown',
+ libraryName: library?.name || 'Unknown',
mediaTags: (libraryItem.media.tags || []).join(', '),
podcastTitle: libraryItem.media.metadata.title,
podcastAuthor: libraryItem.media.metadata.author || '',
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index ce16c2d3..9fe96793 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -50,7 +50,7 @@ class PodcastManager {
}
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
- let index = libraryItem.media.episodes.length + 1
+ let index = Math.max(...libraryItem.media.episodes.filter(ep => ep.index == null || isNaN(ep.index)).map(ep => Number(ep.index))) + 1
for (const ep of episodesToDownload) {
const newPe = new PodcastEpisode()
newPe.setData(ep, index++)
diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js
index c1c10244..7e4759a2 100644
--- a/server/managers/RssFeedManager.js
+++ b/server/managers/RssFeedManager.js
@@ -10,9 +10,10 @@ const Feed = require('../objects/Feed')
class RssFeedManager {
constructor() { }
- validateFeedEntity(feedObj) {
+ async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
- if (!Database.collections.some(li => li.id === feedObj.entityId)) {
+ const collection = await Database.models.collection.getById(feedObj.entityId)
+ if (!collection) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
return false
}
@@ -42,7 +43,7 @@ class RssFeedManager {
const feeds = await Database.models.feed.getOldFeeds()
for (const feed of feeds) {
// Remove invalid feeds
- if (!this.validateFeedEntity(feed)) {
+ if (!await this.validateFeedEntity(feed)) {
await Database.removeFeed(feed.id)
}
}
@@ -101,7 +102,7 @@ class RssFeedManager {
await Database.updateFeed(feed)
}
} else if (feed.entityType === 'collection') {
- const collection = Database.collections.find(c => c.id === feed.entityId)
+ const collection = await Database.models.collection.getById(feed.entityId)
if (collection) {
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
diff --git a/server/models/Collection.js b/server/models/Collection.js
index 3c4c3a71..3c7c1386 100644
--- a/server/models/Collection.js
+++ b/server/models/Collection.js
@@ -92,6 +92,73 @@ module.exports = (sequelize) => {
}
})
}
+
+ /**
+ * Get collection by id
+ * @param {string} collectionId
+ * @returns {Promise |