mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-05-16 16:31:30 +00:00
Add favorite property for items and associated filters.
This commit is contained in:
parent
6d3773a0b8
commit
a5999fb9df
14 changed files with 308 additions and 11 deletions
|
|
@ -102,6 +102,11 @@ class Database {
|
|||
return this.models.libraryItem
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/UserFavorite')} */
|
||||
get userFavoriteModel() {
|
||||
return this.models.userFavorite
|
||||
}
|
||||
|
||||
/** @type {typeof import('./models/PodcastEpisode')} */
|
||||
get podcastEpisodeModel() {
|
||||
return this.models.podcastEpisode
|
||||
|
|
@ -329,6 +334,7 @@ class Database {
|
|||
require('./models/Podcast').init(this.sequelize)
|
||||
require('./models/PodcastEpisode').init(this.sequelize)
|
||||
require('./models/LibraryItem').init(this.sequelize)
|
||||
require('./models/UserFavorite').init(this.sequelize)
|
||||
require('./models/MediaProgress').init(this.sequelize)
|
||||
require('./models/Series').init(this.sequelize)
|
||||
require('./models/BookSeries').init(this.sequelize)
|
||||
|
|
|
|||
|
|
@ -198,6 +198,67 @@ class MeController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* POST: /api/me/item/:id/favorite
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async addFavorite(req, res) {
|
||||
const libraryItemId = req.params.id
|
||||
await Database.userFavoriteModel.create({
|
||||
userId: req.user.id,
|
||||
libraryItemId
|
||||
})
|
||||
|
||||
// Reload favorites for the user
|
||||
await req.user.reload({
|
||||
include: [
|
||||
Database.mediaProgressModel,
|
||||
{
|
||||
model: Database.libraryItemModel,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE: /api/me/item/:id/favorite
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async removeFavorite(req, res) {
|
||||
const libraryItemId = req.params.id
|
||||
await Database.userFavoriteModel.destroy({
|
||||
where: { userId: req.user.id, libraryItemId }
|
||||
})
|
||||
|
||||
// Reload favorites for the user
|
||||
await req.user.reload({
|
||||
include: [
|
||||
Database.mediaProgressModel,
|
||||
{
|
||||
model: Database.libraryItemModel,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* POST: /api/me/item/:id/bookmark
|
||||
*
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ class User extends Model {
|
|||
this.updatedAt
|
||||
/** @type {import('./MediaProgress')[]?} - Only included when extended */
|
||||
this.mediaProgresses
|
||||
/** @type {import('./LibraryItem')[]?} - Only included when extended */
|
||||
this.favorites
|
||||
}
|
||||
|
||||
// Excludes "root" since their can only be 1 root user
|
||||
|
|
@ -357,7 +359,15 @@ class User extends Model {
|
|||
[sequelize.Op.like]: username
|
||||
}
|
||||
},
|
||||
include: this.sequelize.models.mediaProgress
|
||||
include: [
|
||||
this.sequelize.models.mediaProgress,
|
||||
{
|
||||
model: this.sequelize.models.libraryItem,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (user) userCache.set(user)
|
||||
|
|
@ -382,7 +392,15 @@ class User extends Model {
|
|||
[sequelize.Op.like]: email
|
||||
}
|
||||
},
|
||||
include: this.sequelize.models.mediaProgress
|
||||
include: [
|
||||
this.sequelize.models.mediaProgress,
|
||||
{
|
||||
model: this.sequelize.models.libraryItem,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (user) userCache.set(user)
|
||||
|
|
@ -402,7 +420,15 @@ class User extends Model {
|
|||
if (cachedUser) return cachedUser
|
||||
|
||||
const user = await this.findByPk(userId, {
|
||||
include: this.sequelize.models.mediaProgress
|
||||
include: [
|
||||
this.sequelize.models.mediaProgress,
|
||||
{
|
||||
model: this.sequelize.models.libraryItem,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (user) userCache.set(user)
|
||||
|
|
@ -426,7 +452,15 @@ class User extends Model {
|
|||
where: {
|
||||
[sequelize.Op.or]: [{ id: userId }, { 'extraData.oldUserId': userId }]
|
||||
},
|
||||
include: this.sequelize.models.mediaProgress
|
||||
include: [
|
||||
this.sequelize.models.mediaProgress,
|
||||
{
|
||||
model: this.sequelize.models.libraryItem,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (user) userCache.set(user)
|
||||
|
|
@ -447,7 +481,15 @@ class User extends Model {
|
|||
|
||||
const user = await this.findOne({
|
||||
where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub),
|
||||
include: this.sequelize.models.mediaProgress
|
||||
include: [
|
||||
this.sequelize.models.mediaProgress,
|
||||
{
|
||||
model: this.sequelize.models.libraryItem,
|
||||
as: 'favorites',
|
||||
attributes: ['id'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (user) userCache.set(user)
|
||||
|
|
@ -621,6 +663,7 @@ class User extends Model {
|
|||
isOldToken: this.isOldToken,
|
||||
mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
|
||||
seriesHideFromContinueListening: [...seriesHideFromContinueListening],
|
||||
favorites: this.favorites?.map(f => f.id) || [],
|
||||
bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],
|
||||
isActive: this.isActive,
|
||||
isLocked: this.isLocked,
|
||||
|
|
|
|||
66
server/models/UserFavorite.js
Normal file
66
server/models/UserFavorite.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
const { DataTypes, Model } = require('sequelize')
|
||||
|
||||
class UserFavorite extends Model {
|
||||
constructor(values, options) {
|
||||
super(values, options)
|
||||
|
||||
/** @type {UUIDV4} */
|
||||
this.id
|
||||
/** @type {UUIDV4} */
|
||||
this.libraryItemId
|
||||
/** @type {UUIDV4} */
|
||||
this.userId
|
||||
}
|
||||
|
||||
static init(sequelize) {
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
libraryItemId: DataTypes.UUID,
|
||||
userId: DataTypes.UUID
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'userFavorite',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['userId']
|
||||
},
|
||||
{
|
||||
fields: ['libraryItemId']
|
||||
},
|
||||
{
|
||||
unique: true,
|
||||
fields: ['libraryItemId', 'userId'],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { libraryItem, user } = sequelize.models
|
||||
|
||||
libraryItem.hasMany(UserFavorite, {
|
||||
foreignKey: 'libraryItemId',
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
UserFavorite.belongsTo(libraryItem, { foreignKey: 'libraryItemId' })
|
||||
|
||||
user.hasMany(UserFavorite, {
|
||||
foreignKey: 'userId',
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
user.belongsToMany(libraryItem, {
|
||||
through: UserFavorite,
|
||||
foreignKey: 'userId',
|
||||
otherKey: 'libraryItemId',
|
||||
as: 'favorites'
|
||||
})
|
||||
UserFavorite.belongsTo(user, { foreignKey: 'userId' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserFavorite
|
||||
|
|
@ -179,6 +179,8 @@ class ApiRouter {
|
|||
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
||||
this.router.patch('/me/progress/:libraryItemId/:episodeId?', MeController.createUpdateMediaProgress.bind(this))
|
||||
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
||||
this.router.post('/me/item/:id/favorite', MeController.addFavorite.bind(this))
|
||||
this.router.delete('/me/item/:id/favorite', MeController.removeFavorite.bind(this))
|
||||
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
||||
|
|
|
|||
|
|
@ -106,7 +106,11 @@ module.exports = {
|
|||
let mediaWhere = {}
|
||||
const replacements = {}
|
||||
|
||||
if (group === 'progress') {
|
||||
if (group === 'favorite') {
|
||||
mediaWhere['$libraryItem.userFavorites.userId$'] = {
|
||||
[Sequelize.Op.not]: null
|
||||
}
|
||||
} else if (group === 'progress') {
|
||||
if (value === 'not-finished') {
|
||||
mediaWhere['$mediaProgresses.isFinished$'] = {
|
||||
[Sequelize.Op.or]: [null, false]
|
||||
|
|
@ -531,6 +535,15 @@ module.exports = {
|
|||
libraryItemWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
||||
}
|
||||
} else if (filterGroup === 'favorite' && user) {
|
||||
libraryItemIncludes.push({
|
||||
model: Database.userFavoriteModel,
|
||||
attributes: ['userId'],
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
required: true
|
||||
})
|
||||
}
|
||||
|
||||
// When sorting by progress but not filtering by progress, include media progresses
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ module.exports = {
|
|||
let mediaWhere = {}
|
||||
const replacements = {}
|
||||
|
||||
if (['genres', 'tags'].includes(group)) {
|
||||
if (group === 'favorite') {
|
||||
mediaWhere['$libraryItem.userFavorites.userId$'] = {
|
||||
[Sequelize.Op.not]: null
|
||||
}
|
||||
} else if (['genres', 'tags'].includes(group)) {
|
||||
mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = :filterValue)`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
})
|
||||
|
|
@ -172,6 +176,15 @@ module.exports = {
|
|||
libraryItemWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
||||
}
|
||||
} else if (filterGroup === 'favorite' && user) {
|
||||
libraryItemIncludes.push({
|
||||
model: Database.userFavoriteModel,
|
||||
attributes: ['userId'],
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
required: true
|
||||
})
|
||||
}
|
||||
|
||||
const podcastIncludes = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue