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 @@
{{ $strings.MessageNoEpisodes }}
- - - - - + + + + - -
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} returns null if not found + */ + static async getById(collectionId) { + if (!collectionId) return null + const collection = await this.findByPk(collectionId, { + include: { + model: sequelize.models.book, + include: sequelize.models.libraryItem + }, + order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] + }) + if (!collection) return null + return this.getOldCollection(collection) + } + + /** + * Remove all collections belonging to library + * @param {string} libraryId + * @returns {Promise} number of collections destroyed + */ + static async removeAllForLibrary(libraryId) { + if (!libraryId) return 0 + return this.destroy({ + where: { + libraryId + } + }) + } + + /** + * Get all collections for a library + * @param {string} libraryId + * @returns {Promise} + */ + static async getAllForLibrary(libraryId) { + if (!libraryId) return [] + const collections = await this.findAll({ + where: { + libraryId + }, + include: { + model: sequelize.models.book, + include: sequelize.models.libraryItem + }, + order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] + }) + return collections.map(c => this.getOldCollection(c)) + } + + static async getAllForBook(bookId) { + const collections = await this.findAll({ + include: { + model: sequelize.models.book, + where: { + id: bookId + }, + required: true, + include: sequelize.models.libraryItem + }, + order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] + }) + return collections.map(c => this.getOldCollection(c)) + } } Collection.init({ diff --git a/server/models/Library.js b/server/models/Library.js index 439b9a92..7f0f438d 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -4,6 +4,10 @@ const oldLibrary = require('../objects/Library') module.exports = (sequelize) => { class Library extends Model { + /** + * Get all old libraries + * @returns {Promise} + */ static async getAllOldLibraries() { const libraries = await this.findAll({ include: sequelize.models.libraryFolder, @@ -12,6 +16,11 @@ module.exports = (sequelize) => { return libraries.map(lib => this.getOldLibrary(lib)) } + /** + * Convert expanded Library to oldLibrary + * @param {Library} libraryExpanded + * @returns {Promise} + */ static getOldLibrary(libraryExpanded) { const folders = libraryExpanded.libraryFolders.map(folder => { return { @@ -58,6 +67,11 @@ module.exports = (sequelize) => { }) } + /** + * Update library and library folders + * @param {object} oldLibrary + * @returns + */ static async updateFromOld(oldLibrary) { const existingLibrary = await this.findByPk(oldLibrary.id, { include: sequelize.models.libraryFolder @@ -112,6 +126,11 @@ module.exports = (sequelize) => { } } + /** + * Destroy library by id + * @param {string} libraryId + * @returns + */ static removeById(libraryId) { return this.destroy({ where: { @@ -119,6 +138,59 @@ module.exports = (sequelize) => { } }) } + + /** + * Get all library ids + * @returns {Promise} array of library ids + */ + static async getAllLibraryIds() { + const libraries = await this.findAll({ + attributes: ['id'] + }) + return libraries.map(l => l.id) + } + + /** + * Find Library by primary key & return oldLibrary + * @param {string} libraryId + * @returns {Promise} Returns null if not found + */ + static async getOldById(libraryId) { + if (!libraryId) return null + const library = await this.findByPk(libraryId, { + include: sequelize.models.libraryFolder + }) + if (!library) return null + return this.getOldLibrary(library) + } + + /** + * Get the largest value in the displayOrder column + * Used for setting a new libraries display order + * @returns {Promise} + */ + static getMaxDisplayOrder() { + return this.max('displayOrder') || 0 + } + + /** + * Updates displayOrder to be sequential + * Used after removing a library + */ + static async resetDisplayOrder() { + const libraries = await this.findAll({ + order: [['displayOrder', 'ASC']] + }) + for (let i = 0; i < libraries.length; i++) { + const library = libraries[i] + if (library.displayOrder !== i + 1) { + Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`) + await library.update({ displayOrder: i + 1 }).catch((error) => { + Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error) + }) + } + } + } } Library.init({ diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js index 6578dcde..1ba240e7 100644 --- a/server/models/LibraryFolder.js +++ b/server/models/LibraryFolder.js @@ -1,7 +1,18 @@ const { DataTypes, Model } = require('sequelize') module.exports = (sequelize) => { - class LibraryFolder extends Model { } + class LibraryFolder extends Model { + /** + * Gets all library folder path strings + * @returns {Promise} array of library folder paths + */ + static async getAllLibraryFolderPaths() { + const libraryFolders = await this.findAll({ + attributes: ['path'] + }) + return libraryFolders.map(l => l.path) + } + } LibraryFolder.init({ id: { diff --git a/server/models/Playlist.js b/server/models/Playlist.js index 3ae07f5a..bb471ba2 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -1,4 +1,4 @@ -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, Op } = require('sequelize') const Logger = require('../Logger') const oldPlaylist = require('../objects/Playlist') @@ -119,6 +119,146 @@ module.exports = (sequelize) => { } }) } + + /** + * Get playlist by id + * @param {string} playlistId + * @returns {Promise} returns null if not found + */ + static async getById(playlistId) { + if (!playlistId) return null + const playlist = await this.findByPk(playlistId, { + 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 + } + } + ] + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + if (!playlist) return null + return this.getOldPlaylist(playlist) + } + + /** + * Get playlists for user and optionally for library + * @param {string} userId + * @param {[string]} libraryId optional + * @returns {Promise} + */ + static async getPlaylistsForUserAndLibrary(userId, libraryId = null) { + if (!userId && !libraryId) return [] + const whereQuery = {} + if (userId) { + whereQuery.userId = userId + } + if (libraryId) { + whereQuery.libraryId = libraryId + } + const playlists = await this.findAll({ + where: whereQuery, + 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 + } + } + ] + }, + order: [['playlistMediaItems', 'order', 'ASC']] + }) + return playlists.map(p => this.getOldPlaylist(p)) + } + + /** + * Get number of playlists for a user and library + * @param {string} userId + * @param {string} libraryId + * @returns + */ + static async getNumPlaylistsForUserAndLibrary(userId, libraryId) { + return this.count({ + where: { + userId, + libraryId + } + }) + } + + /** + * Get all playlists for mediaItemIds + * @param {string[]} mediaItemIds + * @returns {Promise} + */ + static async getPlaylistsForMediaItemIds(mediaItemIds) { + if (!mediaItemIds?.length) return [] + + const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({ + where: { + mediaItemId: { + [Op.in]: mediaItemIds + } + }, + include: [ + { + model: sequelize.models.playlist, + 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 + } + } + ] + } + } + ], + order: [['playlist', 'playlistMediaItems', 'order', 'ASC']] + }) + return playlistMediaItemsExpanded.map(pmie => { + pmie.playlist.playlistMediaItems = pmie.playlist.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 + } + delete pmi.book + delete pmi.dataValues.book + delete pmi.podcastEpisode + delete pmi.dataValues.podcastEpisode + return pmi + }) + + return this.getOldPlaylist(pmie.playlist) + }) + } } Playlist.init({ diff --git a/server/models/User.js b/server/models/User.js index 32b2b436..6d461110 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,10 +1,14 @@ const uuidv4 = require("uuid").v4 -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, Op } = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') module.exports = (sequelize) => { class User extends Model { + /** + * Get all oldUsers + * @returns {Promise} + */ static async getOldUsers() { const users = await this.findAll({ include: sequelize.models.mediaProgress @@ -89,6 +93,13 @@ module.exports = (sequelize) => { }) } + /** + * Create root user + * @param {string} username + * @param {string} pash + * @param {Auth} auth + * @returns {oldUser} + */ static async createRootUser(username, pash, auth) { const userId = uuidv4() @@ -106,6 +117,95 @@ module.exports = (sequelize) => { await this.createFromOld(newRoot) return newRoot } + + /** + * Get a user by id or by the old database id + * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id + * @param {string} userId + * @returns {Promise} null if not found + */ + static async getUserByIdOrOldId(userId) { + if (!userId) return null + const user = await this.findOne({ + where: { + [Op.or]: [ + { + id: userId + }, + { + extraData: { + [Op.substring]: userId + } + } + ] + }, + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by username case insensitive + * @param {string} username + * @returns {Promise} returns null if not found + */ + static async getUserByUsername(username) { + if (!username) return null + const user = await this.findOne({ + where: { + username: { + [Op.like]: username + } + }, + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get user by id + * @param {string} userId + * @returns {Promise} returns null if not found + */ + static async getUserById(userId) { + if (!userId) return null + const user = await this.findByPk(userId, { + include: sequelize.models.mediaProgress + }) + if (!user) return null + return this.getOldUser(user) + } + + /** + * Get array of user id and username + * @returns {object[]} { id, username } + */ + static async getMinifiedUserObjects() { + const users = await this.findAll({ + attributes: ['id', 'username'] + }) + return users.map(u => { + return { + id: u.id, + username: u.username + } + }) + } + + /** + * Return true if root user exists + * @returns {boolean} + */ + static async getHasRootUser() { + const count = await this.count({ + where: { + type: 'root' + } + }) + return count > 0 + } } User.init({ diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index 25eed681..d7b2acaa 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -523,7 +523,10 @@ class LibraryItem { return this.media.getDirectPlayTracklist(episodeId) } - // Saves metadata.abs file + /** + * Save metadata.json/metadata.abs file + * @returns {boolean} true if saved + */ async saveMetadata() { if (this.mediaType === 'video' || this.mediaType === 'music') return @@ -556,6 +559,7 @@ class LibraryItem { await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) this.libraryFiles.push(newLibraryFile) } + Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) return true }).catch((error) => { diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index d4023740..808bfe32 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -285,7 +285,6 @@ class Podcast { addPodcastEpisode(podcastEpisode) { this.episodes.push(podcastEpisode) - this.reorderEpisodes() } addNewEpisodeFromAudioFile(audioFile, index) { @@ -297,19 +296,6 @@ class Podcast { this.episodes.push(pe) } - reorderEpisodes() { - var hasUpdates = false - - this.episodes = naturalSort(this.episodes).desc((ep) => ep.publishedAt) - for (let i = 0; i < this.episodes.length; i++) { - if (this.episodes[i].index !== (i + 1)) { - this.episodes[i].index = i + 1 - hasUpdates = true - } - } - return hasUpdates - } - removeEpisode(episodeId) { const episode = this.episodes.find(ep => ep.id === episodeId) if (episode) { diff --git a/server/objects/user/User.js b/server/objects/user/User.js index fe1c79e2..8b26f518 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -258,11 +258,15 @@ class User { return hasUpdates } - getDefaultLibraryId(libraries) { + /** + * Get first available library id for user + * + * @param {string[]} libraryIds + * @returns {string|null} + */ + getDefaultLibraryId(libraryIds) { // Libraries should already be in ascending display order, find first accessible - var firstAccessibleLibrary = libraries.find(lib => this.checkCanAccessLibrary(lib.id)) - if (!firstAccessibleLibrary) return null - return firstAccessibleLibrary.id + return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null } // Returns most recent media progress w/ `media` object and optionally an `episode` object diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 0324891a..cfad5545 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -381,19 +381,19 @@ class ApiRouter { async handleDeleteLibraryItem(libraryItem) { // Remove media progress for this library item from all users - for (const user of Database.users) { + const users = await Database.models.user.getOldUsers() + for (const user of users) { for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) { await Database.removeMediaProgress(mediaProgress.id) } } // TODO: Remove open sessions for library item - + let mediaItemIds = [] if (libraryItem.isBook) { // remove book from collections - const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id)) - for (let i = 0; i < collectionsWithBook.length; i++) { - const collection = collectionsWithBook[i] + const collectionsWithBook = await Database.models.collection.getAllForBook(libraryItem.media.id) + for (const collection of collectionsWithBook) { collection.removeBook(libraryItem.id) await Database.removeCollectionBook(collection.id, libraryItem.media.id) SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems)) @@ -401,12 +401,15 @@ class ApiRouter { // Check remove empty series await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id) + + mediaItemIds.push(libraryItem.media.id) + } else if (libraryItem.isPodcast) { + mediaItemIds.push(...libraryItem.media.episodes.map(ep => ep.id)) } // remove item from playlists - const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id)) - for (let i = 0; i < playlistsWithItem.length; i++) { - const playlist = playlistsWithItem[i] + const playlistsWithItem = await Database.models.playlist.getPlaylistsForMediaItemIds(mediaItemIds) + for (const playlist of playlistsWithItem) { playlist.removeItemsForLibraryItem(libraryItem.id) // If playlist is now empty then remove it @@ -462,11 +465,11 @@ class ApiRouter { async getAllSessionsWithUserData() { const sessions = await Database.getPlaybackSessions() sessions.sort((a, b) => b.updatedAt - a.updatedAt) + const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects() return sessions.map(se => { - const user = Database.users.find(u => u.id === se.userId) return { ...se, - user: user ? { id: user.id, username: user.username } : null + user: minifiedUserObjects.find(u => u.id === se.userId) || null } }) } diff --git a/server/scanner/MediaFileScanner.js b/server/scanner/MediaFileScanner.js index d4d041c6..a644a6e0 100644 --- a/server/scanner/MediaFileScanner.js +++ b/server/scanner/MediaFileScanner.js @@ -278,17 +278,17 @@ class MediaFileScanner { const existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino)) if (newAudioFiles.length) { - let newIndex = libraryItem.media.episodes.length + 1 + let newIndex = Math.max(...libraryItem.media.episodes.filter(ep => ep.index == null || isNaN(ep.index)).map(ep => Number(ep.index))) + 1 newAudioFiles.forEach((newAudioFile) => { libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++) }) - libraryItem.media.reorderEpisodes() hasUpdated = true } // Update audio file metadata for audio files already there existingAudioFiles.forEach((af) => { const podcastEpisode = libraryItem.media.findEpisodeWithInode(af.ino) + af.index = 1 if (podcastEpisode?.audioFile.updateFromScan(af)) { hasUpdated = true diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 8d1a8ccf..5aa11ab9 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -66,7 +66,7 @@ class Scanner { } async scanLibraryItemByRequest(libraryItem) { - const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId) + const library = await Database.models.library.getOldById(libraryItem.libraryId) if (!library) { Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`) return ScanResult.NOTHING @@ -552,7 +552,7 @@ class Scanner { for (const folderId in folderGroups) { const libraryId = folderGroups[folderId].libraryId - const library = Database.libraries.find(lib => lib.id === libraryId) + const library = await Database.models.library.getOldById(libraryId) if (!library) { Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) continue;