-
+
+ No Cover
|
-
- {{ item.episode.title || 'Unknown' }}
- {{ item.media.metadata.title }}
-
-
- {{ item.media.metadata.title || 'Unknown' }}
- by {{ item.media.metadata.authorName }}
-
+ {{ item.displayTitle || 'Unknown' }}
+ {{ item.displaySubtitle }}
|
{{ Math.floor(item.progress * 100) }}%
@@ -124,9 +119,6 @@ export default {
mediaProgress() {
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
},
- mediaProgressWithMedia() {
- return this.mediaProgress.filter((mp) => mp.media)
- },
totalListeningTime() {
return this.listeningStats.totalTime || 0
},
diff --git a/client/store/libraries.js b/client/store/libraries.js
index e0151626..fd8af4ae 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -234,6 +234,10 @@ export const mutations = {
setNumUserPlaylists(state, numUserPlaylists) {
state.numUserPlaylists = numUserPlaylists
},
+ removeSeriesFromFilterData(state, seriesId) {
+ if (!seriesId || !state.filterData) return
+ state.filterData.series = state.filterData.series.filter(se => se.id !== seriesId)
+ },
updateFilterDataWithItem(state, libraryItem) {
if (!libraryItem || !state.filterData) return
if (state.currentLibraryId !== libraryItem.libraryId) return
diff --git a/server/Auth.js b/server/Auth.js
index 37ea4bb1..6c7b9891 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -32,7 +32,7 @@ class Auth {
await Database.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user
- const users = await Database.models.user.getOldUsers()
+ const users = await Database.userModel.getOldUsers()
if (users.length) {
for (const user of users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
@@ -100,7 +100,7 @@ class Auth {
return resolve(null)
}
- const user = await Database.models.user.getUserByIdOrOldId(payload.userId)
+ const user = await Database.userModel.getUserByIdOrOldId(payload.userId)
if (user && user.username === payload.username) {
resolve(user)
} else {
@@ -116,7 +116,7 @@ class Auth {
* @returns {object}
*/
async getUserLoginResponsePayload(user) {
- const libraryIds = await Database.models.library.getAllLibraryIds()
+ const libraryIds = await Database.libraryModel.getAllLibraryIds()
return {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
@@ -131,7 +131,7 @@ class Auth {
const username = (req.body.username || '').toLowerCase()
const password = req.body.password || ''
- const user = await Database.models.user.getUserByUsername(username)
+ const user = await Database.userModel.getUserByUsername(username)
if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
@@ -178,7 +178,7 @@ class Auth {
async userChangePassword(req, res) {
var { password, newPassword } = req.body
newPassword = newPassword || ''
- const matchingUser = await Database.models.user.getUserById(req.user.id)
+ const matchingUser = await Database.userModel.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 7dc4f5aa..2ae3585e 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -34,6 +34,100 @@ class Database {
return this.sequelize?.models || {}
}
+ /** @type {typeof import('./models/User')} */
+ get userModel() {
+ return this.models.user
+ }
+
+ /** @type {typeof import('./models/Library')} */
+ get libraryModel() {
+ return this.models.library
+ }
+
+ /** @type {typeof import('./models/Author')} */
+ get authorModel() {
+ return this.models.author
+ }
+
+ /** @type {typeof import('./models/Series')} */
+ get seriesModel() {
+ return this.models.series
+ }
+
+ /** @type {typeof import('./models/Book')} */
+ get bookModel() {
+ return this.models.book
+ }
+
+ /** @type {typeof import('./models/BookSeries')} */
+ get bookSeriesModel() {
+ return this.models.bookSeries
+ }
+
+ /** @type {typeof import('./models/BookAuthor')} */
+ get bookAuthorModel() {
+ return this.models.bookAuthor
+ }
+
+ /** @type {typeof import('./models/Podcast')} */
+ get podcastModel() {
+ return this.models.podcast
+ }
+
+ /** @type {typeof import('./models/PodcastEpisode')} */
+ get podcastEpisodeModel() {
+ return this.models.podcastEpisode
+ }
+
+ /** @type {typeof import('./models/LibraryItem')} */
+ get libraryItemModel() {
+ return this.models.libraryItem
+ }
+
+ /** @type {typeof import('./models/PodcastEpisode')} */
+ get podcastEpisodeModel() {
+ return this.models.podcastEpisode
+ }
+
+ /** @type {typeof import('./models/MediaProgress')} */
+ get mediaProgressModel() {
+ return this.models.mediaProgress
+ }
+
+ /** @type {typeof import('./models/Collection')} */
+ get collectionModel() {
+ return this.models.collection
+ }
+
+ /** @type {typeof import('./models/CollectionBook')} */
+ get collectionBookModel() {
+ return this.models.collectionBook
+ }
+
+ /** @type {typeof import('./models/Playlist')} */
+ get playlistModel() {
+ return this.models.playlist
+ }
+
+ /** @type {typeof import('./models/PlaylistMediaItem')} */
+ get playlistMediaItemModel() {
+ return this.models.playlistMediaItem
+ }
+
+ /** @type {typeof import('./models/Feed')} */
+ get feedModel() {
+ return this.models.feed
+ }
+
+ /** @type {typeof import('./models/Feed')} */
+ get feedEpisodeModel() {
+ return this.models.feedEpisode
+ }
+
+ /**
+ * Check if db file exists
+ * @returns {boolean}
+ */
async checkHasDb() {
if (!await fs.pathExists(this.dbPath)) {
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
@@ -42,6 +136,10 @@ class Database {
return true
}
+ /**
+ * Connect to db, build models and run migrations
+ * @param {boolean} [force=false] Used for testing, drops & re-creates all tables
+ */
async init(force = false) {
this.dbPath = Path.join(global.ConfigPath, 'absdatabase.sqlite')
@@ -58,6 +156,10 @@ class Database {
await this.loadData()
}
+ /**
+ * Connect to db
+ * @returns {boolean}
+ */
async connect() {
Logger.info(`[Database] Initializing db at "${this.dbPath}"`)
this.sequelize = new Sequelize({
@@ -80,39 +182,45 @@ class Database {
}
}
+ /**
+ * Disconnect from db
+ */
async disconnect() {
Logger.info(`[Database] Disconnecting sqlite db`)
await this.sequelize.close()
this.sequelize = null
}
+ /**
+ * Reconnect to db and init
+ */
async reconnect() {
Logger.info(`[Database] Reconnecting sqlite db`)
await this.init()
}
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/User').init(this.sequelize)
+ require('./models/Library').init(this.sequelize)
+ require('./models/LibraryFolder').init(this.sequelize)
+ require('./models/Book').init(this.sequelize)
+ require('./models/Podcast').init(this.sequelize)
+ require('./models/PodcastEpisode').init(this.sequelize)
+ require('./models/LibraryItem').init(this.sequelize)
+ require('./models/MediaProgress').init(this.sequelize)
+ require('./models/Series').init(this.sequelize)
+ require('./models/BookSeries').init(this.sequelize)
require('./models/Author').init(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)
+ require('./models/BookAuthor').init(this.sequelize)
+ require('./models/Collection').init(this.sequelize)
+ require('./models/CollectionBook').init(this.sequelize)
+ require('./models/Playlist').init(this.sequelize)
+ require('./models/PlaylistMediaItem').init(this.sequelize)
+ require('./models/Device').init(this.sequelize)
+ require('./models/PlaybackSession').init(this.sequelize)
+ require('./models/Feed').init(this.sequelize)
+ require('./models/FeedEpisode').init(this.sequelize)
+ require('./models/Setting').init(this.sequelize)
return this.sequelize.sync({ force, alter: false })
}
@@ -481,6 +589,88 @@ class Database {
}
}
}
+
+ removeSeriesFromFilterData(libraryId, seriesId) {
+ if (!this.libraryFilterData[libraryId]) return
+ this.libraryFilterData[libraryId].series = this.libraryFilterData[libraryId].series.filter(se => se.id !== seriesId)
+ }
+
+ addSeriesToFilterData(libraryId, seriesName, seriesId) {
+ if (!this.libraryFilterData[libraryId]) return
+ // Check if series is already added
+ if (this.libraryFilterData[libraryId].series.some(se => se.id === seriesId)) return
+ this.libraryFilterData[libraryId].series.push({
+ id: seriesId,
+ name: seriesName
+ })
+ }
+
+ removeAuthorFromFilterData(libraryId, authorId) {
+ if (!this.libraryFilterData[libraryId]) return
+ this.libraryFilterData[libraryId].authors = this.libraryFilterData[libraryId].authors.filter(au => au.id !== authorId)
+ }
+
+ addAuthorToFilterData(libraryId, authorName, authorId) {
+ if (!this.libraryFilterData[libraryId]) return
+ // Check if author is already added
+ if (this.libraryFilterData[libraryId].authors.some(au => au.id === authorId)) return
+ this.libraryFilterData[libraryId].authors.push({
+ id: authorId,
+ name: authorName
+ })
+ }
+
+ /**
+ * Used when updating items to make sure author id exists
+ * If library filter data is set then use that for check
+ * otherwise lookup in db
+ * @param {string} libraryId
+ * @param {string} authorId
+ * @returns {Promise}
+ */
+ async checkAuthorExists(libraryId, authorId) {
+ if (!this.libraryFilterData[libraryId]) {
+ return this.authorModel.checkExistsById(authorId)
+ }
+ return this.libraryFilterData[libraryId].authors.some(au => au.id === authorId)
+ }
+
+ /**
+ * Used when updating items to make sure series id exists
+ * If library filter data is set then use that for check
+ * otherwise lookup in db
+ * @param {string} libraryId
+ * @param {string} seriesId
+ * @returns {Promise}
+ */
+ async checkSeriesExists(libraryId, seriesId) {
+ if (!this.libraryFilterData[libraryId]) {
+ return this.seriesModel.checkExistsById(seriesId)
+ }
+ return this.libraryFilterData[libraryId].series.some(se => se.id === seriesId)
+ }
+
+ /**
+ * Reset numIssues for library
+ * @param {string} libraryId
+ */
+ async resetLibraryIssuesFilterData(libraryId) {
+ if (!this.libraryFilterData[libraryId]) return // Do nothing if filter data is not set
+
+ this.libraryFilterData[libraryId].numIssues = await this.libraryItemModel.count({
+ where: {
+ libraryId,
+ [Sequelize.Op.or]: [
+ {
+ isMissing: true
+ },
+ {
+ isInvalid: true
+ }
+ ]
+ }
+ })
+ }
}
module.exports = new Database()
\ No newline at end of file
diff --git a/server/Server.js b/server/Server.js
index 87b8e167..83e7ca3e 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -114,10 +114,9 @@ class Server {
await this.backupManager.init()
await this.logManager.init()
- await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
await this.rssFeedManager.init()
- const libraries = await Database.models.library.getAllOldLibraries()
+ const libraries = await Database.libraryModel.getAllOldLibraries()
await this.cronManager.init(libraries)
if (Database.serverSettings.scannerDisableWatcher) {
@@ -254,7 +253,7 @@ class Server {
*/
async cleanUserData() {
// Get all media progress without an associated media item
- const mediaProgressToRemove = await Database.models.mediaProgress.findAll({
+ const mediaProgressToRemove = await Database.mediaProgressModel.findAll({
where: {
'$podcastEpisode.id$': null,
'$book.id$': null
@@ -262,18 +261,18 @@ class Server {
attributes: ['id'],
include: [
{
- model: Database.models.book,
+ model: Database.bookModel,
attributes: ['id']
},
{
- model: Database.models.podcastEpisode,
+ model: Database.podcastEpisodeModel,
attributes: ['id']
}
]
})
if (mediaProgressToRemove.length) {
// Remove media progress
- const mediaProgressRemoved = await Database.models.mediaProgress.destroy({
+ const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
where: {
id: {
[Sequelize.Op.in]: mediaProgressToRemove.map(mp => mp.id)
@@ -286,7 +285,7 @@ class Server {
}
// Remove series from hide from continue listening that no longer exist
- const users = await Database.models.user.getOldUsers()
+ const users = await Database.userModel.getOldUsers()
for (const _user of users) {
let hasUpdated = false
if (_user.seriesHideFromContinueListening.length) {
diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js
index 5133d1cb..1b434d45 100644
--- a/server/controllers/AuthorController.js
+++ b/server/controllers/AuthorController.js
@@ -21,7 +21,7 @@ class AuthorController {
// Used on author landing page to include library items and items grouped in series
if (include.includes('items')) {
- authorJson.libraryItems = await Database.models.libraryItem.getForAuthor(req.author, req.user)
+ authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.user)
if (include.includes('series')) {
const seriesMap = {}
@@ -96,7 +96,7 @@ class AuthorController {
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
if (existingAuthor) {
const bookAuthorsToCreate = []
- const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
+ const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
bookAuthorsToCreate.push({
@@ -113,9 +113,11 @@ class AuthorController {
// Remove old author
await Database.removeAuthor(req.author.id)
SocketAuthority.emitter('author_removed', req.author.toJSON())
+ // Update filter data
+ Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
// Send updated num books for merged author
- const numBooks = await Database.models.libraryItem.getForAuthor(existingAuthor).length
+ const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
res.json({
@@ -130,7 +132,7 @@ class AuthorController {
if (hasUpdated) {
req.author.updatedAt = Date.now()
- const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
+ const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
if (authorNameUpdate) { // Update author name on all books
itemsWithAuthor.forEach(libraryItem => {
libraryItem.media.metadata.updateAuthor(req.author)
@@ -202,7 +204,7 @@ class AuthorController {
await Database.updateAuthor(req.author)
- const numBooks = await Database.models.libraryItem.getForAuthor(req.author).length
+ const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js
index 7b297208..5357a5dc 100644
--- a/server/controllers/CollectionController.js
+++ b/server/controllers/CollectionController.js
@@ -22,10 +22,10 @@ class CollectionController {
}
// Create collection record
- await Database.models.collection.createFromOld(newCollection)
+ await Database.collectionModel.createFromOld(newCollection)
// Get library items in collection
- const libraryItemsInCollection = await Database.models.libraryItem.getForCollection(newCollection)
+ const libraryItemsInCollection = await Database.libraryItemModel.getForCollection(newCollection)
// Create collectionBook records
let order = 1
@@ -50,7 +50,7 @@ class CollectionController {
}
async findAll(req, res) {
- const collectionsExpanded = await Database.models.collection.getOldCollectionsJsonExpanded(req.user)
+ const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user)
res.json({
collections: collectionsExpanded
})
@@ -96,8 +96,8 @@ class CollectionController {
if (req.body.books?.length) {
const collectionBooks = await req.collection.getCollectionBooks({
include: {
- model: Database.models.book,
- include: Database.models.libraryItem
+ model: Database.bookModel,
+ include: Database.libraryItemModel
},
order: [['order', 'ASC']]
})
@@ -143,7 +143,7 @@ class CollectionController {
* @param {*} res
*/
async addBook(req, res) {
- const libraryItem = await Database.models.libraryItem.getOldById(req.body.id)
+ const libraryItem = await Database.libraryItemModel.getOldById(req.body.id)
if (!libraryItem) {
return res.status(404).send('Book not found')
}
@@ -158,7 +158,7 @@ class CollectionController {
}
// Create collectionBook record
- await Database.models.collectionBook.create({
+ await Database.collectionBookModel.create({
collectionId: req.collection.id,
bookId: libraryItem.media.id,
order: collectionBooks.length + 1
@@ -176,7 +176,7 @@ class CollectionController {
* @param {*} res
*/
async removeBook(req, res) {
- const libraryItem = await Database.models.libraryItem.getOldById(req.params.bookId)
+ const libraryItem = await Database.libraryItemModel.getOldById(req.params.bookId)
if (!libraryItem) {
return res.sendStatus(404)
}
@@ -227,14 +227,14 @@ class CollectionController {
}
// Get library items associated with ids
- const libraryItems = await Database.models.libraryItem.findAll({
+ const libraryItems = await Database.libraryItemModel.findAll({
where: {
id: {
[Sequelize.Op.in]: bookIdsToAdd
}
},
include: {
- model: Database.models.book
+ model: Database.bookModel
}
})
@@ -285,14 +285,14 @@ class CollectionController {
}
// Get library items associated with ids
- const libraryItems = await Database.models.libraryItem.findAll({
+ const libraryItems = await Database.libraryItemModel.findAll({
where: {
id: {
[Sequelize.Op.in]: bookIdsToRemove
}
},
include: {
- model: Database.models.book
+ model: Database.bookModel
}
})
@@ -327,7 +327,7 @@ class CollectionController {
async middleware(req, res, next) {
if (req.params.id) {
- const collection = await Database.models.collection.findByPk(req.params.id)
+ const collection = await Database.collectionModel.findByPk(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 8d538489..b2f020ea 100644
--- a/server/controllers/FileSystemController.js
+++ b/server/controllers/FileSystemController.js
@@ -17,7 +17,7 @@ class FileSystemController {
})
// Do not include existing mapped library paths in response
- const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
+ const libraryFoldersPaths = await Database.libraryModelFolder.getAllLibraryFolderPaths()
libraryFoldersPaths.forEach((path) => {
let dir = path || ''
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 70c63708..e64bc8d6 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -8,6 +8,7 @@ const Library = require('../objects/Library')
const libraryHelpers = require('../utils/libraryHelpers')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
+const seriesFilters = require('../utils/queries/seriesFilters')
const { sort, createNewSortInstance } = require('../libs/fastSort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
@@ -15,6 +16,8 @@ const naturalSort = createNewSortInstance({
const Database = require('../Database')
const libraryFilters = require('../utils/queries/libraryFilters')
+const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
+const authorFilters = require('../utils/queries/authorFilters')
class LibraryController {
constructor() { }
@@ -48,7 +51,7 @@ class LibraryController {
const library = new Library()
- let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder()
+ let currentLargestDisplayOrder = await Database.libraryModel.getMaxDisplayOrder()
if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
library.setData(newLibraryPayload)
@@ -67,7 +70,7 @@ class LibraryController {
}
async findAll(req, res) {
- const libraries = await Database.models.library.getAllOldLibraries()
+ const libraries = await Database.libraryModel.getAllOldLibraries()
const librariesAccessible = req.user.librariesAccessible || []
if (librariesAccessible.length) {
@@ -89,7 +92,7 @@ class LibraryController {
return res.json({
filterdata,
issues: filterdata.numIssues,
- numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
+ numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
library: req.library
})
}
@@ -141,17 +144,17 @@ class LibraryController {
for (const folder of library.folders) {
if (!req.body.folders.some(f => f.id === folder.id)) {
// Remove library items in folder
- const libraryItemsInFolder = await Database.models.libraryItem.findAll({
+ const libraryItemsInFolder = await Database.libraryItemModel.findAll({
where: {
libraryFolderId: folder.id
},
attributes: ['id', 'mediaId', 'mediaType'],
include: [
{
- model: Database.models.podcast,
+ model: Database.podcastModel,
attributes: ['id'],
include: {
- model: Database.models.podcastEpisode,
+ model: Database.podcastEpisodeModel,
attributes: ['id']
}
}
@@ -188,6 +191,8 @@ class LibraryController {
return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id)
}
SocketAuthority.emitter('library_updated', library.toJSON(), userFilter)
+
+ await Database.resetLibraryIssuesFilterData(library.id)
}
return res.json(library.toJSON())
}
@@ -205,23 +210,23 @@ class LibraryController {
this.watcher.removeLibrary(library)
// Remove collections for library
- const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id)
+ const numCollectionsRemoved = await Database.collectionModel.removeAllForLibrary(library.id)
if (numCollectionsRemoved) {
Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
}
// Remove items in this library
- const libraryItemsInLibrary = await Database.models.libraryItem.findAll({
+ const libraryItemsInLibrary = await Database.libraryItemModel.findAll({
where: {
libraryId: library.id
},
attributes: ['id', 'mediaId', 'mediaType'],
include: [
{
- model: Database.models.podcast,
+ model: Database.podcastModel,
attributes: ['id'],
include: {
- model: Database.models.podcastEpisode,
+ model: Database.podcastEpisodeModel,
attributes: ['id']
}
}
@@ -243,9 +248,15 @@ class LibraryController {
await Database.removeLibrary(library.id)
// Re-order libraries
- await Database.models.library.resetDisplayOrder()
+ await Database.libraryModel.resetDisplayOrder()
SocketAuthority.emitter('library_removed', libraryJson)
+
+ // Remove library filter data
+ if (Database.libraryFilterData[library.id]) {
+ delete Database.libraryFilterData[library.id]
+ }
+
return res.json(libraryJson)
}
@@ -267,7 +278,7 @@ class LibraryController {
}
payload.offset = payload.page * payload.limit
- const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library, req.user, payload)
+ const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload)
payload.results = libraryItems
payload.total = count
@@ -471,12 +482,13 @@ class LibraryController {
/**
* DELETE: /libraries/:id/issues
* Remove all library items missing or invalid
- * @param {*} req
- * @param {*} res
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
*/
async removeLibraryItemsWithIssues(req, res) {
- const libraryItemsWithIssues = await Database.models.libraryItem.findAll({
+ const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
where: {
+ libraryId: req.library.id,
[Sequelize.Op.or]: [
{
isMissing: true
@@ -489,10 +501,10 @@ class LibraryController {
attributes: ['id', 'mediaId', 'mediaType'],
include: [
{
- model: Database.models.podcast,
+ model: Database.podcastModel,
attributes: ['id'],
include: {
- model: Database.models.podcastEpisode,
+ model: Database.podcastEpisodeModel,
attributes: ['id']
}
}
@@ -507,7 +519,7 @@ class LibraryController {
Logger.info(`[LibraryController] Removing ${libraryItemsWithIssues.length} items with issues`)
for (const libraryItem of libraryItemsWithIssues) {
let mediaItemIds = []
- if (library.isPodcast) {
+ if (req.library.isPodcast) {
mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id)
} else {
mediaItemIds.push(libraryItem.mediaId)
@@ -516,19 +528,22 @@ class LibraryController {
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds)
}
+ // Set numIssues to 0 for library filter data
+ if (Database.libraryFilterData[req.library.id]) {
+ Database.libraryFilterData[req.library.id].numIssues = 0
+ }
+
res.sendStatus(200)
}
/**
- * GET: /api/libraries/:id/series
- * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
- *
- * @param {*} req
- * @param {*} res
- */
+ * GET: /api/libraries/:id/series
+ * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async getAllSeriesForLibrary(req, res) {
- const libraryItems = req.libraryItems
-
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
const payload = {
@@ -543,45 +558,10 @@ class LibraryController {
include: include.join(',')
}
- 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([
- {
- [direction]: (se) => {
- if (payload.sortBy === 'numBooks') {
- return se.books.length
- } else if (payload.sortBy === 'totalDuration') {
- return se.totalDuration
- } else if (payload.sortBy === 'addedAt') {
- return se.addedAt
- } else if (payload.sortBy === 'lastBookUpdated') {
- return Math.max(...(se.books).map(x => x.updatedAt), 0)
- } else if (payload.sortBy === 'lastBookAdded') {
- return Math.max(...(se.books).map(x => x.addedAt), 0)
- } else { // sort by name
- return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
- }
- }
- }
- ])
-
- payload.total = series.length
-
- if (payload.limit) {
- const startIndex = payload.page * payload.limit
- series = series.slice(startIndex, startIndex + payload.limit)
- }
-
- // add rssFeed when "include=rssfeed" is in query string
- if (include.includes('rssfeed')) {
- series = await Promise.all(series.map(async (se) => {
- const feedData = await this.rssFeedManager.findFeedForEntityId(se.id)
- se.rssFeed = feedData?.toJSONMinified() || null
- return se
- }))
- }
+ const offset = payload.page * payload.limit
+ const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset)
+ payload.total = count
payload.results = series
res.json(payload)
}
@@ -644,7 +624,7 @@ class LibraryController {
}
// TODO: Create paginated queries
- let collections = await Database.models.collection.getOldCollectionsJsonExpanded(req.user, req.library.id, include)
+ let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user, req.library.id, include)
payload.total = collections.length
@@ -664,7 +644,7 @@ class LibraryController {
* @param {*} res
*/
async getUserPlaylistsForLibrary(req, res) {
- let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
+ let playlistsForUser = await Database.playlistModel.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded()))
const payload = {
@@ -685,8 +665,8 @@ class LibraryController {
/**
* GET: /api/libraries/:id/filterdata
- * @param {*} req
- * @param {*} res
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
*/
async getLibraryFilterData(req, res) {
const filterData = await libraryFilters.getFilterData(req.library)
@@ -694,44 +674,30 @@ class LibraryController {
}
/**
- * GET: /api/libraries/:id/personalized2
- * TODO: new endpoint
- * @param {*} req
- * @param {*} res
+ * GET: /api/libraries/:id/personalized
+ * Home page shelves
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
*/
async getUserPersonalizedShelves(req, res) {
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
- const shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user, include, limitPerShelf)
+ const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf)
res.json(shelves)
}
- /**
- * GET: /api/libraries/:id/personalized
- * TODO: remove after personalized2 is ready
- * @param {*} req
- * @param {*} res
- */
- async getLibraryUserPersonalizedOptimal(req, res) {
- const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
- const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
-
- const categories = await libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include)
- res.json(categories)
- }
-
/**
* POST: /api/libraries/order
* Change the display order of libraries
- * @param {*} req
- * @param {*} res
+ * @param {import('express').Request} req
+ * @param {import('express').Response} 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()
+ const libraries = await Database.libraryModel.getAllOldLibraries()
const orderdata = req.body
let hasUpdates = false
@@ -759,99 +725,62 @@ class LibraryController {
})
}
- // GET: Global library search
- search(req, res) {
+ /**
+ * GET: /api/libraries/:id/search
+ * Search library items with query
+ * ?q=search
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
+ async search(req, res) {
if (!req.query.q) {
return res.status(400).send('No query string')
}
- const libraryItems = req.libraryItems
- const maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
+ const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
+ const query = req.query.q.trim().toLowerCase()
- const itemMatches = []
- const authorMatches = {}
- const narratorMatches = {}
- const seriesMatches = {}
- const tagMatches = {}
-
- libraryItems.forEach((li) => {
- const queryResult = li.searchQuery(req.query.q)
- if (queryResult.matchKey) {
- itemMatches.push({
- libraryItem: li.toJSONExpanded(),
- matchKey: queryResult.matchKey,
- matchText: queryResult.matchText
- })
- }
- if (queryResult.series?.length) {
- queryResult.series.forEach((se) => {
- if (!seriesMatches[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())
- }
- })
- }
- if (queryResult.authors?.length) {
- queryResult.authors.forEach((au) => {
- if (!authorMatches[au.id]) {
- const _author = Database.authors.find(_au => _au.id === au.id)
- if (_author) {
- authorMatches[au.id] = _author.toJSON()
- authorMatches[au.id].numBooks = 1
- }
- } else {
- authorMatches[au.id].numBooks++
- }
- })
- }
- if (queryResult.tags?.length) {
- queryResult.tags.forEach((tag) => {
- if (!tagMatches[tag]) {
- tagMatches[tag] = { name: tag, books: [li.toJSON()] }
- } else {
- tagMatches[tag].books.push(li.toJSON())
- }
- })
- }
- if (queryResult.narrators?.length) {
- queryResult.narrators.forEach((narrator) => {
- if (!narratorMatches[narrator]) {
- narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
- } else {
- narratorMatches[narrator].books.push(li.toJSON())
- }
- })
- }
- })
- const itemKey = req.library.mediaType
- const results = {
- [itemKey]: itemMatches.slice(0, maxResults),
- tags: Object.values(tagMatches).slice(0, maxResults),
- authors: Object.values(authorMatches).slice(0, maxResults),
- series: Object.values(seriesMatches).slice(0, maxResults),
- narrators: Object.values(narratorMatches).slice(0, maxResults)
- }
- res.json(results)
+ const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
+ res.json(matches)
}
+ /**
+ * GET: /api/libraries/:id/stats
+ * Get stats for library
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async stats(req, res) {
- var libraryItems = req.libraryItems
- var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems)
- var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems)
- var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
- var sizeStats = libraryHelpers.getItemSizeStats(libraryItems)
- var stats = {
- totalItems: libraryItems.length,
- totalAuthors: Object.keys(authorsWithCount).length,
- totalGenres: Object.keys(genresWithCount).length,
- totalDuration: durationStats.totalDuration,
- longestItems: durationStats.longestItems,
- numAudioTracks: durationStats.numAudioTracks,
- totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems),
- largestItems: sizeStats.largestItems,
- authorsWithCount,
- genresWithCount
+ const stats = {
+ largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
+ }
+
+ if (req.library.isBook) {
+ const authors = await authorFilters.getAuthorsWithCount(req.library.id)
+ const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
+ const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
+ const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)
+
+ stats.totalAuthors = authors.length
+ stats.authorsWithCount = authors
+ stats.totalGenres = genres.length
+ stats.genresWithCount = genres
+ stats.totalItems = bookStats.totalItems
+ stats.longestItems = longestBooks
+ stats.totalSize = bookStats.totalSize
+ stats.totalDuration = bookStats.totalDuration
+ stats.numAudioTracks = bookStats.numAudioFiles
+ } else {
+ const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
+ const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
+ const longestPodcasts = await libraryItemsPodcastFilters.getLongestPodcasts(req.library.id, 10)
+
+ stats.totalGenres = genres.length
+ stats.genresWithCount = genres
+ stats.totalItems = podcastStats.totalItems
+ stats.longestItems = longestPodcasts
+ stats.totalSize = podcastStats.totalSize
+ stats.totalDuration = podcastStats.totalDuration
+ stats.numAudioTracks = podcastStats.numAudioFiles
}
res.json(stats)
}
@@ -859,18 +788,18 @@ class LibraryController {
/**
* GET: /api/libraries/:id/authors
* Get authors for library
- * @param {*} req
- * @param {*} res
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
*/
async getAuthors(req, res) {
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
- const authors = await Database.models.author.findAll({
+ const authors = await Database.authorModel.findAll({
where: {
libraryId: req.library.id
},
replacements,
include: {
- model: Database.models.book,
+ model: Database.bookModel,
attributes: ['id', 'tags', 'explicit'],
where: bookWhere,
required: true,
@@ -903,12 +832,12 @@ class LibraryController {
*/
async getNarrators(req, res) {
// Get all books with narrators
- const booksWithNarrators = await Database.models.book.findAll({
+ const booksWithNarrators = await Database.bookModel.findAll({
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('narrators')), {
[Sequelize.Op.gt]: 0
}),
include: {
- model: Database.models.libraryItem,
+ model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId: req.library.id
@@ -975,7 +904,7 @@ class LibraryController {
await libraryItem.media.update({
narrators: libraryItem.media.narrators
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
itemsUpdated.push(oldLibraryItem)
}
@@ -1015,7 +944,7 @@ class LibraryController {
await libraryItem.media.update({
narrators: libraryItem.media.narrators
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
itemsUpdated.push(oldLibraryItem)
}
@@ -1048,10 +977,16 @@ class LibraryController {
}
res.sendStatus(200)
await this.scanner.scan(req.library, options)
+ await Database.resetLibraryIssuesFilterData(req.library.id)
Logger.info('[LibraryController] Scan complete')
}
- // GET: api/libraries/:id/recent-episode
+ /**
+ * GET: /api/libraries/:id/recent-episodes
+ * Used for latest page
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async getRecentEpisodes(req, res) {
if (!req.library.isPodcast) {
return res.sendStatus(404)
@@ -1059,40 +994,37 @@ class LibraryController {
const payload = {
episodes: [],
- total: 0,
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
}
- var allUnfinishedEpisodes = []
- for (const libraryItem of req.libraryItems) {
- const unfinishedEpisodes = libraryItem.media.episodes.filter(ep => {
- const userProgress = req.user.getMediaProgress(libraryItem.id, ep.id)
- return !userProgress || !userProgress.isFinished
- }).map(_ep => {
- const ep = _ep.toJSONExpanded()
- ep.podcast = libraryItem.media.toJSONMinified()
- ep.libraryItemId = libraryItem.id
- ep.libraryId = libraryItem.libraryId
- return ep
- })
- allUnfinishedEpisodes.push(...unfinishedEpisodes)
- }
-
- payload.total = allUnfinishedEpisodes.length
-
- allUnfinishedEpisodes = sort(allUnfinishedEpisodes).desc(ep => ep.publishedAt)
-
- if (payload.limit) {
- var startIndex = payload.page * payload.limit
- allUnfinishedEpisodes = allUnfinishedEpisodes.slice(startIndex, startIndex + payload.limit)
- }
- payload.episodes = allUnfinishedEpisodes
+ const offset = payload.page * payload.limit
+ payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, req.library, payload.limit, offset)
res.json(payload)
}
- getOPMLFile(req, res) {
- const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems)
+ /**
+ * GET: /api/libraries/:id/opml
+ * Get OPML file for a podcast library
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
+ async getOPMLFile(req, res) {
+ const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.user)
+ const podcasts = await Database.podcastModel.findAll({
+ attributes: ['id', 'feedURL', 'title', 'description', 'itunesPageURL', 'language'],
+ where: userPermissionPodcastWhere.podcastWhere,
+ replacements: userPermissionPodcastWhere.replacements,
+ include: {
+ model: Database.libraryItemModel,
+ attributes: ['id', 'libraryId'],
+ where: {
+ libraryId: req.library.id
+ }
+ }
+ })
+
+ const opmlText = this.podcastManager.generateOPMLFileText(podcasts)
res.type('application/xml')
res.send(opmlText)
}
@@ -1109,7 +1041,7 @@ class LibraryController {
return res.sendStatus(403)
}
- const library = await Database.models.library.getOldById(req.params.id)
+ const library = await Database.libraryModel.getOldById(req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
@@ -1122,9 +1054,9 @@ class LibraryController {
/**
* Middleware that is not using libraryItems from memory
- * @param {*} req
- * @param {*} res
- * @param {*} next
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ * @param {import('express').NextFunction} next
*/
async middlewareNew(req, res, next) {
if (!req.user.checkCanAccessLibrary(req.params.id)) {
@@ -1132,7 +1064,7 @@ class LibraryController {
return res.sendStatus(403)
}
- const library = await Database.models.library.getOldById(req.params.id)
+ const library = await Database.libraryModel.getOldById(req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index bdfc9322..f2396b30 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -78,6 +78,7 @@ class LibraryItemController {
Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error)
})
}
+ await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.sendStatus(200)
}
@@ -124,7 +125,7 @@ class LibraryItemController {
// Book specific - Get all series being removed from this item
let seriesRemoved = []
if (libraryItem.isBook && mediaPayload.metadata?.series) {
- const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
+ const seriesIdsInUpdate = mediaPayload.metadata.series?.map(se => se.id) || []
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
}
@@ -135,7 +136,7 @@ class LibraryItemController {
if (seriesRemoved.length) {
// Check remove empty series
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
- await this.checkRemoveEmptySeries(seriesRemoved)
+ await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
}
if (isPodcastAutoDownloadUpdated) {
@@ -313,7 +314,7 @@ class LibraryItemController {
return res.status(400).send('Invalid request body')
}
- const itemsToDelete = await Database.models.libraryItem.getAllOldLibraryItems({
+ const itemsToDelete = await Database.libraryItemModel.getAllOldLibraryItems({
id: libraryItemIds
})
@@ -332,6 +333,8 @@ class LibraryItemController {
})
}
}
+
+ await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.sendStatus(200)
}
@@ -346,13 +349,26 @@ class LibraryItemController {
for (const updatePayload of updatePayloads) {
const mediaPayload = updatePayload.mediaPayload
- const libraryItem = await Database.models.libraryItem.getOldById(updatePayload.id)
+ const libraryItem = await Database.libraryItemModel.getOldById(updatePayload.id)
if (!libraryItem) return null
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
+ let seriesRemoved = []
+ if (libraryItem.isBook && mediaPayload.metadata?.series) {
+ const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
+ seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
+ }
+
if (libraryItem.media.update(mediaPayload)) {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
+
+ if (seriesRemoved.length) {
+ // Check remove empty series
+ Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
+ await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
+ }
+
await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
itemsUpdated++
@@ -371,7 +387,7 @@ class LibraryItemController {
if (!libraryItemIds.length) {
return res.status(403).send('Invalid payload')
}
- const libraryItems = await Database.models.libraryItem.getAllOldLibraryItems({
+ const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({
id: libraryItemIds
})
res.json({
@@ -443,9 +459,11 @@ class LibraryItemController {
await this.scanner.scanLibraryItemByRequest(libraryItem)
}
}
+
+ await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
}
- // POST: api/items/:id/scan (admin)
+ // POST: api/items/:id/scan
async scan(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user)
@@ -458,6 +476,7 @@ class LibraryItemController {
}
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
+ await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
res.json({
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
})
@@ -681,7 +700,7 @@ class LibraryItemController {
}
async middleware(req, res, next) {
- req.libraryItem = await Database.models.libraryItem.getOldById(req.params.id)
+ req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!req.libraryItem?.media) return res.sendStatus(404)
// Check user can access this library item
diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js
index ec2d146a..f5e252c1 100644
--- a/server/controllers/MeController.js
+++ b/server/controllers/MeController.js
@@ -59,7 +59,7 @@ class MeController {
// PATCH: api/me/progress/:id
async createUpdateMediaProgress(req, res) {
- const libraryItem = await Database.models.libraryItem.getOldById(req.params.id)
+ const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) {
return res.status(404).send('Item not found')
}
@@ -75,7 +75,7 @@ class MeController {
// PATCH: api/me/progress/:id/:episodeId
async createUpdateEpisodeMediaProgress(req, res) {
const episodeId = req.params.episodeId
- const libraryItem = await Database.models.libraryItem.getOldById(req.params.id)
+ const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) {
return res.status(404).send('Item not found')
}
@@ -101,7 +101,7 @@ class MeController {
let shouldUpdate = false
for (const itemProgress of itemProgressPayloads) {
- const libraryItem = await Database.models.libraryItem.getOldById(itemProgress.libraryItemId)
+ const libraryItem = await Database.libraryItemModel.getOldById(itemProgress.libraryItemId)
if (libraryItem) {
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
@@ -122,7 +122,7 @@ class MeController {
// POST: api/me/item/:id/bookmark
async createBookmark(req, res) {
- if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404)
+ if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const { time, title } = req.body
const bookmark = req.user.createBookmark(req.params.id, time, title)
@@ -133,7 +133,7 @@ class MeController {
// PATCH: api/me/item/:id/bookmark
async updateBookmark(req, res) {
- if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404)
+ if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const { time, title } = req.body
if (!req.user.findBookmark(req.params.id, time)) {
@@ -151,7 +151,7 @@ class MeController {
// DELETE: api/me/item/:id/bookmark/:time
async removeBookmark(req, res) {
- if (!await Database.models.libraryItem.checkExistsById(req.params.id)) return res.sendStatus(404)
+ if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
const time = Number(req.params.time)
if (isNaN(time)) return res.sendStatus(500)
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index 603a9976..94ab9ea4 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -38,7 +38,7 @@ class MiscController {
const libraryId = req.body.library
const folderId = req.body.folder
- const library = await Database.models.library.getOldById(libraryId)
+ const library = await Database.libraryModel.getOldById(libraryId)
if (!library) {
return res.status(404).send(`Library not found with id ${libraryId}`)
}
@@ -177,7 +177,7 @@ class MiscController {
}
const tags = []
- const books = await Database.models.book.findAll({
+ const books = await Database.bookModel.findAll({
attributes: ['tags'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0
@@ -189,7 +189,7 @@ class MiscController {
}
}
- const podcasts = await Database.models.podcast.findAll({
+ const podcasts = await Database.podcastModel.findAll({
attributes: ['tags'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0
@@ -248,7 +248,7 @@ class MiscController {
await libraryItem.media.update({
tags: libraryItem.media.tags
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
}
@@ -289,7 +289,7 @@ class MiscController {
await libraryItem.media.update({
tags: libraryItem.media.tags
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
}
@@ -311,7 +311,7 @@ class MiscController {
return res.sendStatus(404)
}
const genres = []
- const books = await Database.models.book.findAll({
+ const books = await Database.bookModel.findAll({
attributes: ['genres'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
[Sequelize.Op.gt]: 0
@@ -323,7 +323,7 @@ class MiscController {
}
}
- const podcasts = await Database.models.podcast.findAll({
+ const podcasts = await Database.podcastModel.findAll({
attributes: ['genres'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
[Sequelize.Op.gt]: 0
@@ -382,7 +382,7 @@ class MiscController {
await libraryItem.media.update({
genres: libraryItem.media.genres
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
}
@@ -423,7 +423,7 @@ class MiscController {
await libraryItem.media.update({
genres: libraryItem.media.genres
})
- const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
+ const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
}
diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js
index afbc7f1f..16514ef8 100644
--- a/server/controllers/PlaylistController.js
+++ b/server/controllers/PlaylistController.js
@@ -22,11 +22,11 @@ class PlaylistController {
}
// Create Playlist record
- const newPlaylist = await Database.models.playlist.createFromOld(oldPlaylist)
+ const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
// Lookup all library items in playlist
const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId).filter(i => i)
- const libraryItemsInPlaylist = await Database.models.libraryItem.findAll({
+ const libraryItemsInPlaylist = await Database.libraryItemModel.findAll({
where: {
id: libraryItemIds
}
@@ -62,7 +62,7 @@ class PlaylistController {
* @param {*} res
*/
async findAllForUser(req, res) {
- const playlistsForUser = await Database.models.playlist.findAll({
+ const playlistsForUser = await Database.playlistModel.findAll({
where: {
userId: req.user.id
}
@@ -106,7 +106,7 @@ class PlaylistController {
// If array of items is passed in then update order of playlist media items
const libraryItemIds = req.body.items?.map(i => i.libraryItemId).filter(i => i) || []
if (libraryItemIds.length) {
- const libraryItems = await Database.models.libraryItem.findAll({
+ const libraryItems = await Database.libraryItemModel.findAll({
where: {
id: libraryItemIds
}
@@ -173,14 +173,14 @@ class PlaylistController {
* @param {*} res
*/
async addItem(req, res) {
- const oldPlaylist = await Database.models.playlist.getById(req.playlist.id)
+ const oldPlaylist = await Database.playlistModel.getById(req.playlist.id)
const itemToAdd = req.body
if (!itemToAdd.libraryItemId) {
return res.status(400).send('Request body has no libraryItemId')
}
- const libraryItem = await Database.models.libraryItem.getOldById(itemToAdd.libraryItemId)
+ const libraryItem = await Database.libraryItemModel.getOldById(itemToAdd.libraryItemId)
if (!libraryItem) {
return res.status(400).send('Library item not found')
}
@@ -217,7 +217,7 @@ class PlaylistController {
* @param {*} res
*/
async removeItem(req, res) {
- const oldLibraryItem = await Database.models.libraryItem.getOldById(req.params.libraryItemId)
+ const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId)
if (!oldLibraryItem) {
return res.status(404).send('Library item not found')
}
@@ -281,7 +281,7 @@ class PlaylistController {
}
// Find all library items
- const libraryItems = await Database.models.libraryItem.findAll({
+ const libraryItems = await Database.libraryItemModel.findAll({
where: {
id: libraryItemIds
}
@@ -345,7 +345,7 @@ class PlaylistController {
}
// Find all library items
- const libraryItems = await Database.models.libraryItem.findAll({
+ const libraryItems = await Database.libraryItemModel.findAll({
where: {
id: libraryItemIds
}
@@ -391,7 +391,7 @@ class PlaylistController {
* @param {*} res
*/
async createFromCollection(req, res) {
- const collection = await Database.models.collection.findByPk(req.params.collectionId)
+ const collection = await Database.collectionModel.findByPk(req.params.collectionId)
if (!collection) {
return res.status(404).send('Collection not found')
}
@@ -416,7 +416,7 @@ class PlaylistController {
})
// Create Playlist record
- const newPlaylist = await Database.models.playlist.createFromOld(oldPlaylist)
+ const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
// Create PlaylistMediaItem records
const mediaItemsToAdd = []
@@ -438,7 +438,7 @@ class PlaylistController {
async middleware(req, res, next) {
if (req.params.id) {
- const playlist = await Database.models.playlist.findByPk(req.params.id)
+ const playlist = await Database.playlistModel.findByPk(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 76781516..ba91ad94 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -19,7 +19,7 @@ class PodcastController {
}
const payload = req.body
- const library = await Database.models.library.getOldById(payload.libraryId)
+ const library = await Database.libraryModel.getOldById(payload.libraryId)
if (!library) {
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
return res.status(404).send('Library not found')
@@ -34,7 +34,7 @@ class PodcastController {
const podcastPath = filePathToPOSIX(payload.path)
// Check if a library item with this podcast folder exists already
- const existingLibraryItem = (await Database.models.libraryItem.count({
+ const existingLibraryItem = (await Database.libraryItemModel.count({
where: {
path: podcastPath
}
@@ -272,13 +272,13 @@ class PodcastController {
}
// Update/remove playlists that had this podcast episode
- const playlistMediaItems = await Database.models.playlistMediaItem.findAll({
+ const playlistMediaItems = await Database.playlistMediaItemModel.findAll({
where: {
mediaItemId: episodeId
},
include: {
- model: Database.models.playlist,
- include: Database.models.playlistMediaItem
+ model: Database.playlistModel,
+ include: Database.playlistMediaItemModel
}
})
for (const pmi of playlistMediaItems) {
@@ -297,7 +297,7 @@ class PodcastController {
}
// Remove media progress for this episode
- const mediaProgressRemoved = await Database.models.mediaProgress.destroy({
+ const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
where: {
mediaItemId: episode.id
}
@@ -312,7 +312,7 @@ class PodcastController {
}
async middleware(req, res, next) {
- const item = await Database.models.libraryItem.getOldById(req.params.id)
+ const item = await Database.libraryItemModel.getOldById(req.params.id)
if (!item?.media) return res.sendStatus(404)
if (!item.isPodcast) {
diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js
index 51f818ef..95cd5e2d 100644
--- a/server/controllers/RSSFeedController.js
+++ b/server/controllers/RSSFeedController.js
@@ -9,7 +9,7 @@ class RSSFeedController {
async openRSSFeedForItem(req, res) {
const options = req.body || {}
- const item = await Database.models.libraryItem.getOldById(req.params.itemId)
+ const item = await Database.libraryItemModel.getOldById(req.params.itemId)
if (!item) return res.sendStatus(404)
// Check user can access this library item
@@ -46,7 +46,7 @@ class RSSFeedController {
async openRSSFeedForCollection(req, res) {
const options = req.body || {}
- const collection = await Database.models.collection.findByPk(req.params.collectionId)
+ const collection = await Database.collectionModel.findByPk(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 698f58d7..928095a6 100644
--- a/server/controllers/SessionController.js
+++ b/server/controllers/SessionController.js
@@ -49,7 +49,7 @@ class SessionController {
return res.sendStatus(404)
}
- const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
+ const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects()
const openSessions = this.playbackSessionManager.sessions.map(se => {
return {
...se.toJSON(),
diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js
index 9c28d618..9561e892 100644
--- a/server/controllers/ToolsController.js
+++ b/server/controllers/ToolsController.js
@@ -106,7 +106,7 @@ class ToolsController {
}
if (req.params.id) {
- const item = await Database.models.libraryItem.getOldById(req.params.id)
+ const item = await Database.libraryItemModel.getOldById(req.params.id)
if (!item?.media) return res.sendStatus(404)
// Check user can access this library item
diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js
index 0928b46f..a3f70e20 100644
--- a/server/controllers/UserController.js
+++ b/server/controllers/UserController.js
@@ -17,7 +17,7 @@ class UserController {
const includes = (req.query.include || '').split(',').map(i => i.trim())
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
- const allUsers = await Database.models.user.getOldUsers()
+ const allUsers = await Database.userModel.getOldUsers()
const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
if (includes.includes('latestSession')) {
@@ -32,20 +32,67 @@ class UserController {
})
}
+ /**
+ * GET: /api/users/:id
+ * Get a single user toJSONForBrowser
+ * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
+ *
+ * @param {import("express").Request} req
+ * @param {import("express").Response} 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)
}
- res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
+ // Get user media progress with associated mediaItem
+ const mediaProgresses = await Database.mediaProgressModel.findAll({
+ where: {
+ userId: req.reqUser.id
+ },
+ include: [
+ {
+ model: Database.bookModel,
+ attributes: ['id', 'title', 'coverPath', 'updatedAt']
+ },
+ {
+ model: Database.podcastEpisodeModel,
+ attributes: ['id', 'title'],
+ include: {
+ model: Database.podcastModel,
+ attributes: ['id', 'title', 'coverPath', 'updatedAt']
+ }
+ }
+ ]
+ })
+
+ const oldMediaProgresses = mediaProgresses.map(mp => {
+ const oldMediaProgress = mp.getOldMediaProgress()
+ oldMediaProgress.displayTitle = mp.mediaItem?.title
+ if (mp.mediaItem?.podcast) {
+ oldMediaProgress.displaySubtitle = mp.mediaItem.podcast?.title
+ oldMediaProgress.coverPath = mp.mediaItem.podcast?.coverPath
+ oldMediaProgress.mediaUpdatedAt = mp.mediaItem.podcast?.updatedAt
+ } else if (mp.mediaItem) {
+ oldMediaProgress.coverPath = mp.mediaItem.coverPath
+ oldMediaProgress.mediaUpdatedAt = mp.mediaItem.updatedAt
+ }
+ return oldMediaProgress
+ })
+
+ const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot)
+
+ userJson.mediaProgress = oldMediaProgresses
+
+ res.json(userJson)
}
async create(req, res) {
const account = req.body
const username = account.username
- const usernameExists = await Database.models.user.getUserByUsername(username)
+ const usernameExists = await Database.userModel.getUserByUsername(username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@@ -80,7 +127,7 @@ class UserController {
var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) {
- const usernameExists = await Database.models.user.getUserByUsername(account.username)
+ const usernameExists = await Database.userModel.getUserByUsername(account.username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@@ -122,7 +169,7 @@ class UserController {
// Todo: check if user is logged in and cancel streams
// Remove user playlists
- const userPlaylists = await Database.models.playlist.findAll({
+ const userPlaylists = await Database.playlistModel.findAll({
where: {
userId: user.id
}
@@ -186,7 +233,7 @@ class UserController {
}
if (req.params.id) {
- req.reqUser = await Database.models.user.getUserById(req.params.id)
+ req.reqUser = await Database.userModel.getUserById(req.params.id)
if (!req.reqUser) {
return res.sendStatus(404)
}
diff --git a/server/db/libraryItem.db.js b/server/db/libraryItem.db.js
index 3f08bf06..335a52a1 100644
--- a/server/db/libraryItem.db.js
+++ b/server/db/libraryItem.db.js
@@ -5,23 +5,23 @@ const { Sequelize } = require('sequelize')
const Database = require('../Database')
const getLibraryItemMinified = (libraryItemId) => {
- return Database.models.libraryItem.findByPk(libraryItemId, {
+ return Database.libraryItemModel.findByPk(libraryItemId, {
include: [
{
- model: Database.models.book,
+ model: Database.bookModel,
attributes: [
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags'
],
include: [
{
- model: Database.models.author,
+ model: Database.authorModel,
attributes: ['id', 'name'],
through: {
attributes: []
}
},
{
- model: Database.models.series,
+ model: Database.seriesModel,
attributes: ['id', 'name'],
through: {
attributes: ['sequence']
@@ -30,7 +30,7 @@ const getLibraryItemMinified = (libraryItemId) => {
]
},
{
- model: Database.models.podcast,
+ model: Database.podcastModel,
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']
@@ -41,19 +41,19 @@ const getLibraryItemMinified = (libraryItemId) => {
}
const getLibraryItemExpanded = (libraryItemId) => {
- return Database.models.libraryItem.findByPk(libraryItemId, {
+ return Database.libraryItemModel.findByPk(libraryItemId, {
include: [
{
- model: Database.models.book,
+ model: Database.bookModel,
include: [
{
- model: Database.models.author,
+ model: Database.authorModel,
through: {
attributes: []
}
},
{
- model: Database.models.series,
+ model: Database.seriesModel,
through: {
attributes: ['sequence']
}
@@ -61,10 +61,10 @@ const getLibraryItemExpanded = (libraryItemId) => {
]
},
{
- model: Database.models.podcast,
+ model: Database.podcastModel,
include: [
{
- model: Database.models.podcastEpisode
+ model: Database.podcastEpisodeModel
}
]
},
diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js
index 4be27754..8b2b1849 100644
--- a/server/managers/CronManager.js
+++ b/server/managers/CronManager.js
@@ -77,7 +77,7 @@ class CronManager {
async initPodcastCrons() {
const cronExpressionMap = {}
- const podcastsWithAutoDownload = await Database.models.podcast.findAll({
+ const podcastsWithAutoDownload = await Database.podcastModel.findAll({
where: {
autoDownloadEpisodes: true,
autoDownloadSchedule: {
@@ -85,7 +85,7 @@ class CronManager {
}
},
include: {
- model: Database.models.libraryItem
+ model: Database.libraryItemModel
}
})
@@ -139,7 +139,7 @@ class CronManager {
// Get podcast library items to check
const libraryItems = []
for (const libraryItemId of libraryItemIds) {
- const libraryItem = await Database.models.libraryItem.getOldById(libraryItemId)
+ const libraryItem = await Database.libraryItemModel.getOldById(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
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index 5f3ab238..9007261a 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -18,7 +18,7 @@ class NotificationManager {
if (!Database.notificationSettings.isUseable) return
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
- const library = await Database.models.library.getOldById(libraryItem.libraryId)
+ const library = await Database.libraryModel.getOldById(libraryItem.libraryId)
const eventData = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js
index 06e50fdc..a64acc18 100644
--- a/server/managers/PlaybackSessionManager.js
+++ b/server/managers/PlaybackSessionManager.js
@@ -265,7 +265,7 @@ class PlaybackSessionManager {
}
async syncSession(user, session, syncData) {
- const libraryItem = await Database.models.libraryItem.getOldById(session.libraryItemId)
+ const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId)
if (!libraryItem) {
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
return null
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index 743cb60b..d8d32c5c 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -150,7 +150,7 @@ class PodcastManager {
return false
}
- const libraryItem = await Database.models.libraryItem.getOldById(this.currentDownload.libraryItem.id)
+ const libraryItem = await Database.libraryItemModel.getOldById(this.currentDownload.libraryItem.id)
if (!libraryItem) {
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
return false
@@ -372,8 +372,13 @@ class PodcastManager {
}
}
- generateOPMLFileText(libraryItems) {
- return opmlGenerator.generate(libraryItems)
+ /**
+ * OPML file string for podcasts in a library
+ * @param {import('../models/Podcast')[]} podcasts
+ * @returns {string} XML string
+ */
+ generateOPMLFileText(podcasts) {
+ return opmlGenerator.generate(podcasts)
}
getDownloadQueueDetails(libraryId = null) {
diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js
index 835f408a..e13c9aa5 100644
--- a/server/managers/RssFeedManager.js
+++ b/server/managers/RssFeedManager.js
@@ -13,13 +13,13 @@ class RssFeedManager {
async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
- const collection = await Database.models.collection.getOldById(feedObj.entityId)
+ const collection = await Database.collectionModel.getOldById(feedObj.entityId)
if (!collection) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
return false
}
} else if (feedObj.entityType === 'libraryItem') {
- const libraryItemExists = await Database.models.libraryItem.checkExistsById(feedObj.entityId)
+ const libraryItemExists = await Database.libraryItemModel.checkExistsById(feedObj.entityId)
if (!libraryItemExists) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
return false
@@ -41,7 +41,7 @@ class RssFeedManager {
* Validate all feeds and remove invalid
*/
async init() {
- const feeds = await Database.models.feed.getOldFeeds()
+ const feeds = await Database.feedModel.getOldFeeds()
for (const feed of feeds) {
// Remove invalid feeds
if (!await this.validateFeedEntity(feed)) {
@@ -56,7 +56,7 @@ class RssFeedManager {
* @returns {Promise} oldFeed
*/
findFeedForEntityId(entityId) {
- return Database.models.feed.findOneOld({ entityId })
+ return Database.feedModel.findOneOld({ entityId })
}
/**
@@ -65,7 +65,7 @@ class RssFeedManager {
* @returns {Promise} oldFeed
*/
findFeedBySlug(slug) {
- return Database.models.feed.findOneOld({ slug })
+ return Database.feedModel.findOneOld({ slug })
}
/**
@@ -74,7 +74,7 @@ class RssFeedManager {
* @returns {Promise} oldFeed
*/
findFeed(id) {
- return Database.models.feed.findByPkOld(id)
+ return Database.feedModel.findByPkOld(id)
}
async getFeed(req, res) {
@@ -103,7 +103,7 @@ class RssFeedManager {
await Database.updateFeed(feed)
}
} else if (feed.entityType === 'collection') {
- const collection = await Database.models.collection.findByPk(feed.entityId)
+ const collection = await Database.collectionModel.findByPk(feed.entityId)
if (collection) {
const collectionExpanded = await collection.getOldJsonExpanded()
diff --git a/server/models/Author.js b/server/models/Author.js
index 88dc3eea..9eeda5bf 100644
--- a/server/models/Author.js
+++ b/server/models/Author.js
@@ -83,6 +83,15 @@ class Author extends Model {
})
}
+ /**
+ * Check if author exists
+ * @param {string} authorId
+ * @returns {Promise}
+ */
+ static async checkExistsById(authorId) {
+ return (await this.count({ where: { id: authorId } })) > 0
+ }
+
/**
* Initialize model
* @param {import('../Database').sequelize} sequelize
diff --git a/server/models/Book.js b/server/models/Book.js
index b17afc6d..415064de 100644
--- a/server/models/Book.js
+++ b/server/models/Book.js
@@ -1,178 +1,231 @@
const { DataTypes, Model } = require('sequelize')
const Logger = require('../Logger')
-module.exports = (sequelize) => {
- class Book extends Model {
- static getOldBook(libraryItemExpanded) {
- const bookExpanded = libraryItemExpanded.media
- let authors = []
- if (bookExpanded.authors?.length) {
- authors = bookExpanded.authors.map(au => {
- return {
- id: au.id,
- name: au.name
- }
- })
- } else if (bookExpanded.bookAuthors?.length) {
- authors = bookExpanded.bookAuthors.map(ba => {
- if (ba.author) {
- return {
- id: ba.author.id,
- name: ba.author.name
- }
- } else {
- Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba)
- return null
- }
- }).filter(a => a)
- }
+class Book extends Model {
+ constructor(values, options) {
+ super(values, options)
- let series = []
- if (bookExpanded.series?.length) {
- series = bookExpanded.series.map(se => {
- return {
- id: se.id,
- name: se.name,
- sequence: se.bookSeries.sequence
- }
- })
- } else if (bookExpanded.bookSeries?.length) {
- series = bookExpanded.bookSeries.map(bs => {
- if (bs.series) {
- return {
- id: bs.series.id,
- name: bs.series.name,
- sequence: bs.sequence
- }
- } else {
- Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs)
- return null
- }
- }).filter(s => s)
- }
+ /** @type {UUIDV4} */
+ this.id
+ /** @type {string} */
+ this.title
+ /** @type {string} */
+ this.titleIgnorePrefix
+ /** @type {string} */
+ this.publishedYear
+ /** @type {string} */
+ this.publishedDate
+ /** @type {string} */
+ this.publisher
+ /** @type {string} */
+ this.description
+ /** @type {string} */
+ this.isbn
+ /** @type {string} */
+ this.asin
+ /** @type {string} */
+ this.language
+ /** @type {boolean} */
+ this.explicit
+ /** @type {boolean} */
+ this.abridged
+ /** @type {string} */
+ this.coverPath
+ /** @type {number} */
+ this.duration
+ /** @type {Object} */
+ this.narrators
+ /** @type {Object} */
+ this.audioFiles
+ /** @type {Object} */
+ this.ebookFile
+ /** @type {Object} */
+ this.chapters
+ /** @type {Object} */
+ this.tags
+ /** @type {Object} */
+ this.genres
+ /** @type {Date} */
+ this.updatedAt
+ /** @type {Date} */
+ this.createdAt
+ }
- 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
+ static getOldBook(libraryItemExpanded) {
+ const bookExpanded = libraryItemExpanded.media
+ let authors = []
+ if (bookExpanded.authors?.length) {
+ authors = bookExpanded.authors.map(au => {
+ return {
+ id: au.id,
+ name: au.name
}
- }
- }
-
- /**
- * @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
})
+ } else if (bookExpanded.bookAuthors?.length) {
+ authors = bookExpanded.bookAuthors.map(ba => {
+ if (ba.author) {
+ return {
+ id: ba.author.id,
+ name: ba.author.name
+ }
+ } else {
+ Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba)
+ return null
+ }
+ }).filter(a => a)
}
- static getFromOld(oldBook) {
- return {
- id: oldBook.id,
- title: oldBook.metadata.title,
- titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix,
- 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,
- duration: oldBook.duration,
- audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [],
- chapters: oldBook.chapters,
- tags: oldBook.tags,
- genres: oldBook.metadata.genres
+ let series = []
+ if (bookExpanded.series?.length) {
+ series = bookExpanded.series.map(se => {
+ return {
+ id: se.id,
+ name: se.name,
+ sequence: se.bookSeries.sequence
+ }
+ })
+ } else if (bookExpanded.bookSeries?.length) {
+ series = bookExpanded.bookSeries.map(bs => {
+ if (bs.series) {
+ return {
+ id: bs.series.id,
+ name: bs.series.name,
+ sequence: bs.sequence
+ }
+ } else {
+ Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs)
+ return null
+ }
+ }).filter(s => s)
+ }
+
+ 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
}
}
}
- Book.init({
- id: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- primaryKey: true
- },
- title: DataTypes.STRING,
- titleIgnorePrefix: 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,
- duration: DataTypes.FLOAT,
-
- narrators: DataTypes.JSON,
- audioFiles: DataTypes.JSON,
- ebookFile: DataTypes.JSON,
- chapters: DataTypes.JSON,
- tags: DataTypes.JSON,
- genres: DataTypes.JSON
- }, {
- sequelize,
- modelName: 'book',
- indexes: [
- {
- fields: [{
- name: 'title',
- collate: 'NOCASE'
- }]
- },
- {
- fields: [{
- name: 'titleIgnorePrefix',
- collate: 'NOCASE'
- }]
- },
- {
- fields: ['publishedYear']
- },
- {
- fields: ['duration']
+ /**
+ * @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
+ })
+ }
- return Book
-}
\ No newline at end of file
+ static getFromOld(oldBook) {
+ return {
+ id: oldBook.id,
+ title: oldBook.metadata.title,
+ titleIgnorePrefix: oldBook.metadata.titleIgnorePrefix,
+ 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,
+ duration: oldBook.duration,
+ audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [],
+ chapters: oldBook.chapters,
+ tags: oldBook.tags,
+ genres: oldBook.metadata.genres
+ }
+ }
+
+ /**
+ * Initialize model
+ * @param {import('../Database').sequelize} sequelize
+ */
+ static init(sequelize) {
+ super.init({
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ title: DataTypes.STRING,
+ titleIgnorePrefix: 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,
+ duration: DataTypes.FLOAT,
+
+ narrators: DataTypes.JSON,
+ audioFiles: DataTypes.JSON,
+ ebookFile: DataTypes.JSON,
+ chapters: DataTypes.JSON,
+ tags: DataTypes.JSON,
+ genres: DataTypes.JSON
+ }, {
+ sequelize,
+ modelName: 'book',
+ indexes: [
+ {
+ fields: [{
+ name: 'title',
+ collate: 'NOCASE'
+ }]
+ },
+ {
+ fields: [{
+ name: 'titleIgnorePrefix',
+ collate: 'NOCASE'
+ }]
+ },
+ {
+ fields: ['publishedYear']
+ },
+ {
+ fields: ['duration']
+ }
+ ]
+ })
+ }
+}
+
+module.exports = Book
\ No newline at end of file
diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js
index c425d2fd..9f8860ee 100644
--- a/server/models/BookAuthor.js
+++ b/server/models/BookAuthor.js
@@ -1,41 +1,57 @@
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
- })
- }
+class BookAuthor extends Model {
+ constructor(values, options) {
+ super(values, options)
+
+ /** @type {UUIDV4} */
+ this.id
+ /** @type {UUIDV4} */
+ this.bookId
+ /** @type {UUIDV4} */
+ this.authorId
+ /** @type {Date} */
+ this.createdAt
}
- BookAuthor.init({
- id: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- primaryKey: true
- }
- }, {
- sequelize,
- modelName: 'bookAuthor',
- timestamps: true,
- updatedAt: false
- })
+ static removeByIds(authorId = null, bookId = null) {
+ const where = {}
+ if (authorId) where.authorId = authorId
+ if (bookId) where.bookId = bookId
+ return this.destroy({
+ where
+ })
+ }
- // 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 })
+ /**
+ * Initialize model
+ * @param {import('../Database').sequelize} sequelize
+ */
+ static init(sequelize) {
+ super.init({
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ }
+ }, {
+ sequelize,
+ modelName: 'bookAuthor',
+ timestamps: true,
+ updatedAt: false
+ })
- book.hasMany(BookAuthor)
- BookAuthor.belongsTo(book)
+ // 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 })
- author.hasMany(BookAuthor)
- BookAuthor.belongsTo(author)
+ book.hasMany(BookAuthor)
+ BookAuthor.belongsTo(book)
- return BookAuthor
-}
\ No newline at end of file
+ author.hasMany(BookAuthor)
+ BookAuthor.belongsTo(author)
+ }
+}
+module.exports = BookAuthor
\ No newline at end of file
diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js
index ba6581f2..680ad0c1 100644
--- a/server/models/BookSeries.js
+++ b/server/models/BookSeries.js
@@ -1,42 +1,61 @@
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
- })
- }
+class BookSeries extends Model {
+ constructor(values, options) {
+ super(values, options)
+
+ /** @type {UUIDV4} */
+ this.id
+ /** @type {string} */
+ this.sequence
+ /** @type {UUIDV4} */
+ this.bookId
+ /** @type {UUIDV4} */
+ this.seriesId
+ /** @type {Date} */
+ this.createdAt
}
- BookSeries.init({
- id: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- primaryKey: true
- },
- sequence: DataTypes.STRING
- }, {
- sequelize,
- modelName: 'bookSeries',
- timestamps: true,
- updatedAt: false
- })
+ static removeByIds(seriesId = null, bookId = null) {
+ const where = {}
+ if (seriesId) where.seriesId = seriesId
+ if (bookId) where.bookId = bookId
+ return this.destroy({
+ where
+ })
+ }
- // 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 })
+ /**
+ * Initialize model
+ * @param {import('../Database').sequelize} sequelize
+ */
+ static init(sequelize) {
+ super.init({
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ sequence: DataTypes.STRING
+ }, {
+ sequelize,
+ modelName: 'bookSeries',
+ timestamps: true,
+ updatedAt: false
+ })
- book.hasMany(BookSeries)
- BookSeries.belongsTo(book)
+ // 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 })
- series.hasMany(BookSeries)
- BookSeries.belongsTo(series)
+ book.hasMany(BookSeries)
+ BookSeries.belongsTo(book)
- return BookSeries
-}
\ No newline at end of file
+ series.hasMany(BookSeries)
+ BookSeries.belongsTo(series)
+ }
+}
+
+module.exports = BookSeries
\ No newline at end of file
diff --git a/server/models/Collection.js b/server/models/Collection.js
index f4aa9b46..9d3a8e0a 100644
--- a/server/models/Collection.js
+++ b/server/models/Collection.js
@@ -1,151 +1,97 @@
const { DataTypes, Model, Sequelize } = require('sequelize')
const oldCollection = require('../objects/Collection')
-const { areEquivalent } = require('../utils/index')
-module.exports = (sequelize) => {
- class Collection extends Model {
- /**
- * Get all old collections
- * @returns {Promise}
- */
- static async getOldCollections() {
- const collections = await this.findAll({
- include: {
- model: sequelize.models.book,
- include: sequelize.models.libraryItem
+
+class Collection extends Model {
+ constructor(values, options) {
+ super(values, options)
+
+ /** @type {UUIDV4} */
+ this.id
+ /** @type {string} */
+ this.name
+ /** @type {string} */
+ this.description
+ /** @type {UUIDV4} */
+ this.libraryId
+ /** @type {Date} */
+ this.updatedAt
+ /** @type {Date} */
+ this.createdAt
+ }
+ /**
+ * Get all old collections
+ * @returns {Promise}
+ */
+ static async getOldCollections() {
+ const collections = await this.findAll({
+ include: {
+ model: this.sequelize.models.book,
+ include: this.sequelize.models.libraryItem
+ },
+ order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
+ })
+ return collections.map(c => this.getOldCollection(c))
+ }
+
+ /**
+ * Get all old collections toJSONExpanded, items filtered for user permissions
+ * @param {[oldUser]} user
+ * @param {[string]} libraryId
+ * @param {[string[]]} include
+ * @returns {Promise |