mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-08-02 22:51:45 +00:00
Before the new minified listening stats path could return 502 because PlaybackSession.afterFind assumed Sequelize model instances even for raw stats rows, and some payloads could serialize invalid totals. After this hotfix the stats helper returns successfully again, with the current dataset benchmarking at about 124ms for full stats and 101ms for minified stats instead of crashing.
295 lines
8.6 KiB
JavaScript
295 lines
8.6 KiB
JavaScript
const { DataTypes, Model } = require('sequelize')
|
|
|
|
const oldPlaybackSession = require('../objects/PlaybackSession')
|
|
|
|
class PlaybackSession extends Model {
|
|
constructor(values, options) {
|
|
super(values, options)
|
|
|
|
/** @type {UUIDV4} */
|
|
this.id
|
|
/** @type {UUIDV4} */
|
|
this.mediaItemId
|
|
/** @type {string} */
|
|
this.mediaItemType
|
|
/** @type {string} */
|
|
this.displayTitle
|
|
/** @type {string} */
|
|
this.displayAuthor
|
|
/** @type {number} */
|
|
this.duration
|
|
/** @type {number} */
|
|
this.playMethod
|
|
/** @type {string} */
|
|
this.mediaPlayer
|
|
/** @type {number} */
|
|
this.startTime
|
|
/** @type {number} */
|
|
this.currentTime
|
|
/** @type {string} */
|
|
this.serverVersion
|
|
/** @type {string} */
|
|
this.coverPath
|
|
/** @type {number} */
|
|
this.timeListening
|
|
/** @type {Object} */
|
|
this.mediaMetadata
|
|
/** @type {string} */
|
|
this.date
|
|
/** @type {string} */
|
|
this.dayOfWeek
|
|
/** @type {Object} */
|
|
this.extraData
|
|
/** @type {UUIDV4} */
|
|
this.userId
|
|
/** @type {UUIDV4} */
|
|
this.deviceId
|
|
/** @type {UUIDV4} */
|
|
this.libraryId
|
|
/** @type {Date} */
|
|
this.updatedAt
|
|
/** @type {Date} */
|
|
this.createdAt
|
|
}
|
|
|
|
static async getOldPlaybackSessions(where = null, options = {}) {
|
|
const {
|
|
limit = null,
|
|
offset = null,
|
|
order = [['updatedAt', 'DESC']],
|
|
includeDevice = true
|
|
} = options
|
|
|
|
const playbackSessions = await this.findAll({
|
|
where,
|
|
limit,
|
|
offset,
|
|
order,
|
|
include: includeDevice
|
|
? [
|
|
{
|
|
model: this.sequelize.models.device
|
|
}
|
|
]
|
|
: undefined
|
|
})
|
|
return playbackSessions.map((session) => this.getOldPlaybackSession(session))
|
|
}
|
|
|
|
static countWithWhere(where = null) {
|
|
return this.count({ where })
|
|
}
|
|
|
|
static getPlaybackSessionsForStats(where = null) {
|
|
return this.findAll({
|
|
where,
|
|
order: [['updatedAt', 'DESC']],
|
|
attributes: [
|
|
'mediaItemId',
|
|
'mediaItemType',
|
|
'displayTitle',
|
|
'displayAuthor',
|
|
'timeListening',
|
|
'mediaMetadata',
|
|
'date',
|
|
'dayOfWeek',
|
|
'updatedAt',
|
|
'extraData'
|
|
],
|
|
raw: true
|
|
})
|
|
}
|
|
|
|
static async getById(sessionId) {
|
|
const playbackSession = await this.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: this.sequelize.models.device
|
|
}
|
|
]
|
|
})
|
|
if (!playbackSession) return null
|
|
return this.getOldPlaybackSession(playbackSession)
|
|
}
|
|
|
|
static getOldPlaybackSession(playbackSessionExpanded) {
|
|
const isPodcastEpisode = playbackSessionExpanded.mediaItemType === 'podcastEpisode'
|
|
|
|
return new oldPlaybackSession({
|
|
id: playbackSessionExpanded.id,
|
|
userId: playbackSessionExpanded.userId,
|
|
libraryId: playbackSessionExpanded.libraryId,
|
|
libraryItemId: playbackSessionExpanded.extraData?.libraryItemId || null,
|
|
bookId: isPodcastEpisode ? null : playbackSessionExpanded.mediaItemId,
|
|
episodeId: isPodcastEpisode ? playbackSessionExpanded.mediaItemId : null,
|
|
mediaType: isPodcastEpisode ? 'podcast' : 'book',
|
|
mediaMetadata: playbackSessionExpanded.mediaMetadata,
|
|
chapters: null,
|
|
displayTitle: playbackSessionExpanded.displayTitle,
|
|
displayAuthor: playbackSessionExpanded.displayAuthor,
|
|
coverPath: playbackSessionExpanded.coverPath,
|
|
duration: playbackSessionExpanded.duration,
|
|
playMethod: playbackSessionExpanded.playMethod,
|
|
mediaPlayer: playbackSessionExpanded.mediaPlayer,
|
|
deviceInfo: playbackSessionExpanded.device?.getOldDevice() || null,
|
|
serverVersion: playbackSessionExpanded.serverVersion,
|
|
date: playbackSessionExpanded.date,
|
|
dayOfWeek: playbackSessionExpanded.dayOfWeek,
|
|
timeListening: playbackSessionExpanded.timeListening,
|
|
startTime: playbackSessionExpanded.startTime,
|
|
currentTime: playbackSessionExpanded.currentTime,
|
|
startedAt: playbackSessionExpanded.createdAt.valueOf(),
|
|
updatedAt: playbackSessionExpanded.updatedAt.valueOf()
|
|
})
|
|
}
|
|
|
|
static removeById(sessionId) {
|
|
return this.destroy({
|
|
where: {
|
|
id: sessionId
|
|
}
|
|
})
|
|
}
|
|
|
|
static createFromOld(oldPlaybackSession) {
|
|
const playbackSession = this.getFromOld(oldPlaybackSession)
|
|
return this.upsert(playbackSession, {
|
|
silent: true
|
|
})
|
|
}
|
|
|
|
static updateFromOld(oldPlaybackSession) {
|
|
const playbackSession = this.getFromOld(oldPlaybackSession)
|
|
return this.update(playbackSession, {
|
|
where: {
|
|
id: playbackSession.id
|
|
},
|
|
silent: true
|
|
})
|
|
}
|
|
|
|
static getFromOld(oldPlaybackSession) {
|
|
return {
|
|
id: oldPlaybackSession.id,
|
|
mediaItemId: oldPlaybackSession.episodeId || oldPlaybackSession.bookId,
|
|
mediaItemType: oldPlaybackSession.episodeId ? 'podcastEpisode' : 'book',
|
|
libraryId: oldPlaybackSession.libraryId,
|
|
displayTitle: oldPlaybackSession.displayTitle,
|
|
displayAuthor: oldPlaybackSession.displayAuthor,
|
|
duration: oldPlaybackSession.duration,
|
|
playMethod: oldPlaybackSession.playMethod,
|
|
mediaPlayer: oldPlaybackSession.mediaPlayer,
|
|
startTime: oldPlaybackSession.startTime,
|
|
currentTime: oldPlaybackSession.currentTime,
|
|
serverVersion: oldPlaybackSession.serverVersion || null,
|
|
createdAt: oldPlaybackSession.startedAt,
|
|
updatedAt: oldPlaybackSession.updatedAt,
|
|
userId: oldPlaybackSession.userId,
|
|
deviceId: oldPlaybackSession.deviceInfo?.id || null,
|
|
timeListening: oldPlaybackSession.timeListening,
|
|
coverPath: oldPlaybackSession.coverPath,
|
|
mediaMetadata: oldPlaybackSession.mediaMetadata,
|
|
date: oldPlaybackSession.date,
|
|
dayOfWeek: oldPlaybackSession.dayOfWeek,
|
|
extraData: {
|
|
libraryItemId: oldPlaybackSession.libraryItemId
|
|
}
|
|
}
|
|
}
|
|
|
|
getMediaItem(options) {
|
|
if (!this.mediaItemType) return Promise.resolve(null)
|
|
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaItemType)}`
|
|
return this[mixinMethodName](options)
|
|
}
|
|
|
|
/**
|
|
* Initialize model
|
|
* @param {import('../Database').sequelize} sequelize
|
|
*/
|
|
static init(sequelize) {
|
|
super.init(
|
|
{
|
|
id: {
|
|
type: DataTypes.UUID,
|
|
defaultValue: DataTypes.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
mediaItemId: DataTypes.UUID,
|
|
mediaItemType: DataTypes.STRING,
|
|
displayTitle: DataTypes.STRING,
|
|
displayAuthor: DataTypes.STRING,
|
|
duration: DataTypes.FLOAT,
|
|
playMethod: DataTypes.INTEGER,
|
|
mediaPlayer: DataTypes.STRING,
|
|
startTime: DataTypes.FLOAT,
|
|
currentTime: DataTypes.FLOAT,
|
|
serverVersion: DataTypes.STRING,
|
|
coverPath: DataTypes.STRING,
|
|
timeListening: DataTypes.FLOAT,
|
|
mediaMetadata: DataTypes.JSON,
|
|
date: DataTypes.STRING,
|
|
dayOfWeek: DataTypes.STRING,
|
|
extraData: DataTypes.JSON
|
|
},
|
|
{
|
|
sequelize,
|
|
modelName: 'playbackSession'
|
|
}
|
|
)
|
|
|
|
const { book, podcastEpisode, user, device, library } = sequelize.models
|
|
|
|
user.hasMany(PlaybackSession)
|
|
PlaybackSession.belongsTo(user)
|
|
|
|
device.hasMany(PlaybackSession)
|
|
PlaybackSession.belongsTo(device)
|
|
|
|
library.hasMany(PlaybackSession)
|
|
PlaybackSession.belongsTo(library)
|
|
|
|
book.hasMany(PlaybackSession, {
|
|
foreignKey: 'mediaItemId',
|
|
constraints: false,
|
|
scope: {
|
|
mediaItemType: 'book'
|
|
}
|
|
})
|
|
PlaybackSession.belongsTo(book, { foreignKey: 'mediaItemId', constraints: false })
|
|
|
|
podcastEpisode.hasOne(PlaybackSession, {
|
|
foreignKey: 'mediaItemId',
|
|
constraints: false,
|
|
scope: {
|
|
mediaItemType: 'podcastEpisode'
|
|
}
|
|
})
|
|
PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
|
|
|
PlaybackSession.addHook('afterFind', (findResult) => {
|
|
if (!findResult) return
|
|
|
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
|
|
|
for (const instance of findResult) {
|
|
const values = instance?.dataValues
|
|
|
|
if (instance.mediaItemType === 'book' && instance.book !== undefined) {
|
|
instance.mediaItem = instance.book
|
|
if (values) values.mediaItem = values.book
|
|
} else if (instance.mediaItemType === 'podcastEpisode' && instance.podcastEpisode !== undefined) {
|
|
instance.mediaItem = instance.podcastEpisode
|
|
if (values) values.mediaItem = values.podcastEpisode
|
|
}
|
|
// To prevent mistakes:
|
|
delete instance.book
|
|
if (values) delete values.book
|
|
delete instance.podcastEpisode
|
|
if (values) delete values.podcastEpisode
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
module.exports = PlaybackSession
|