2023-07-05 01:14:44 +02:00
const { DataTypes , Model } = require ( 'sequelize' )
2024-10-22 00:48:02 +02:00
const Logger = require ( '../Logger' )
const { isNullOrNaN } = require ( '../utils' )
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
class MediaProgress extends Model {
constructor ( values , options ) {
super ( values , options )
/** @type {UUIDV4} */
this . id
/** @type {UUIDV4} */
this . mediaItemId
/** @type {string} */
this . mediaItemType
/** @type {number} */
this . duration
/** @type {number} */
this . currentTime
/** @type {boolean} */
this . isFinished
/** @type {boolean} */
this . hideFromContinueListening
/** @type {string} */
this . ebookLocation
/** @type {number} */
this . ebookProgress
/** @type {Date} */
this . finishedAt
/** @type {Object} */
this . extraData
/** @type {UUIDV4} */
this . userId
/** @type {Date} */
this . updatedAt
/** @type {Date} */
this . createdAt
}
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
static upsertFromOld ( oldMediaProgress ) {
const mediaProgress = this . getFromOld ( oldMediaProgress )
return this . upsert ( mediaProgress )
}
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
static getFromOld ( oldMediaProgress ) {
return {
id : oldMediaProgress . id ,
userId : oldMediaProgress . userId ,
mediaItemId : oldMediaProgress . mediaItemId ,
mediaItemType : oldMediaProgress . mediaItemType ,
duration : oldMediaProgress . duration ,
currentTime : oldMediaProgress . currentTime ,
ebookLocation : oldMediaProgress . ebookLocation || null ,
ebookProgress : oldMediaProgress . ebookProgress || null ,
isFinished : ! ! oldMediaProgress . isFinished ,
hideFromContinueListening : ! ! oldMediaProgress . hideFromContinueListening ,
finishedAt : oldMediaProgress . finishedAt ,
createdAt : oldMediaProgress . startedAt || oldMediaProgress . lastUpdate ,
updatedAt : oldMediaProgress . lastUpdate ,
extraData : {
libraryItemId : oldMediaProgress . libraryItemId ,
progress : oldMediaProgress . progress
}
2023-07-05 01:14:44 +02:00
}
2023-08-16 23:38:48 +02:00
}
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
static removeById ( mediaProgressId ) {
return this . destroy ( {
where : {
id : mediaProgressId
}
} )
2023-07-05 01:14:44 +02:00
}
2023-08-16 23:38:48 +02:00
getMediaItem ( options ) {
if ( ! this . mediaItemType ) return Promise . resolve ( null )
const mixinMethodName = ` get ${ this . sequelize . uppercaseFirst ( this . mediaItemType ) } `
return this [ mixinMethodName ] ( options )
}
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
/ * *
* Initialize model
2024-05-29 00:24:02 +02:00
*
2023-08-16 23:38:48 +02:00
* Polymorphic association : Book has many MediaProgress . PodcastEpisode has many MediaProgress .
* @ see https : //sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
2024-05-29 00:24:02 +02:00
*
* @ param { import ( '../Database' ) . sequelize } sequelize
2023-08-16 23:38:48 +02:00
* /
static init ( sequelize ) {
2024-05-29 00:24:02 +02:00
super . init (
{
id : {
type : DataTypes . UUID ,
defaultValue : DataTypes . UUIDV4 ,
primaryKey : true
} ,
2024-11-17 22:45:21 +01:00
mediaItemId : DataTypes . UUID ,
2024-05-29 00:24:02 +02:00
mediaItemType : DataTypes . STRING ,
duration : DataTypes . FLOAT ,
currentTime : DataTypes . FLOAT ,
isFinished : DataTypes . BOOLEAN ,
hideFromContinueListening : DataTypes . BOOLEAN ,
ebookLocation : DataTypes . STRING ,
ebookProgress : DataTypes . FLOAT ,
finishedAt : DataTypes . DATE ,
extraData : DataTypes . JSON
2023-08-16 23:38:48 +02:00
} ,
2024-05-29 00:24:02 +02:00
{
sequelize ,
modelName : 'mediaProgress' ,
indexes : [
{
fields : [ 'updatedAt' ]
}
]
}
)
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
const { book , podcastEpisode , user } = sequelize . models
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
book . hasMany ( MediaProgress , {
foreignKey : 'mediaItemId' ,
constraints : false ,
scope : {
mediaItemType : 'book'
}
} )
MediaProgress . belongsTo ( book , { foreignKey : 'mediaItemId' , constraints : false } )
podcastEpisode . hasMany ( MediaProgress , {
foreignKey : 'mediaItemId' ,
constraints : false ,
scope : {
mediaItemType : 'podcastEpisode'
}
} )
MediaProgress . belongsTo ( podcastEpisode , { foreignKey : 'mediaItemId' , constraints : false } )
2023-07-05 01:14:44 +02:00
2024-05-29 00:24:02 +02:00
MediaProgress . addHook ( 'afterFind' , ( findResult ) => {
2023-08-16 23:38:48 +02:00
if ( ! findResult ) return
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
if ( ! Array . isArray ( findResult ) ) findResult = [ findResult ]
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
for ( const instance of findResult ) {
if ( instance . mediaItemType === 'book' && instance . book !== undefined ) {
instance . mediaItem = instance . book
instance . dataValues . mediaItem = instance . dataValues . book
} else if ( instance . mediaItemType === 'podcastEpisode' && instance . podcastEpisode !== undefined ) {
instance . mediaItem = instance . podcastEpisode
instance . dataValues . mediaItem = instance . dataValues . podcastEpisode
}
// To prevent mistakes:
delete instance . book
delete instance . dataValues . book
delete instance . podcastEpisode
delete instance . dataValues . podcastEpisode
2023-07-05 01:14:44 +02:00
}
2023-08-16 23:38:48 +02:00
} )
2023-07-05 01:14:44 +02:00
2023-08-16 23:38:48 +02:00
user . hasMany ( MediaProgress , {
onDelete : 'CASCADE'
} )
MediaProgress . belongsTo ( user )
}
2024-08-10 22:46:04 +02:00
getOldMediaProgress ( ) {
const isPodcastEpisode = this . mediaItemType === 'podcastEpisode'
return {
id : this . id ,
userId : this . userId ,
libraryItemId : this . extraData ? . libraryItemId || null ,
episodeId : isPodcastEpisode ? this . mediaItemId : null ,
mediaItemId : this . mediaItemId ,
mediaItemType : this . mediaItemType ,
duration : this . duration ,
progress : this . extraData ? . progress || 0 ,
currentTime : this . currentTime ,
isFinished : ! ! this . isFinished ,
hideFromContinueListening : ! ! this . hideFromContinueListening ,
ebookLocation : this . ebookLocation ,
ebookProgress : this . ebookProgress ,
lastUpdate : this . updatedAt . valueOf ( ) ,
startedAt : this . createdAt . valueOf ( ) ,
finishedAt : this . finishedAt ? . valueOf ( ) || null
}
}
2024-08-11 18:53:30 +02:00
2024-10-22 00:48:02 +02:00
get progress ( ) {
// Value between 0 and 1
if ( ! this . duration ) return 0
return Math . max ( 0 , Math . min ( this . currentTime / this . duration , 1 ) )
}
2024-08-11 18:53:30 +02:00
/ * *
* Apply update to media progress
*
2024-10-22 00:48:02 +02:00
* @ param { import ( './User' ) . ProgressUpdatePayload } progressPayload
2024-08-11 18:53:30 +02:00
* @ returns { Promise < MediaProgress > }
* /
applyProgressUpdate ( progressPayload ) {
if ( ! this . extraData ) this . extraData = { }
if ( progressPayload . isFinished !== undefined ) {
if ( progressPayload . isFinished && ! this . isFinished ) {
this . finishedAt = Date . now ( )
this . extraData . progress = 1
this . changed ( 'extraData' , true )
delete progressPayload . finishedAt
} else if ( ! progressPayload . isFinished && this . isFinished ) {
this . finishedAt = null
this . extraData . progress = 0
this . currentTime = 0
this . changed ( 'extraData' , true )
delete progressPayload . finishedAt
delete progressPayload . currentTime
}
} else if ( ! isNaN ( progressPayload . progress ) && progressPayload . progress !== this . progress ) {
// Old model stored progress on object
this . extraData . progress = Math . min ( 1 , Math . max ( 0 , progressPayload . progress ) )
this . changed ( 'extraData' , true )
}
this . set ( progressPayload )
// Reset hideFromContinueListening if the progress has changed
if ( this . changed ( 'currentTime' ) && ! progressPayload . hideFromContinueListening ) {
this . hideFromContinueListening = false
}
const timeRemaining = this . duration - this . currentTime
2024-10-22 00:48:02 +02:00
// Check if progress is far enough to mark as finished
2024-10-25 00:19:51 +02:00
// - If markAsFinishedPercentComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 10 seconds)
2024-10-22 00:48:02 +02:00
let shouldMarkAsFinished = false
2024-10-26 00:27:50 +02:00
if ( this . duration ) {
if ( ! isNullOrNaN ( progressPayload . markAsFinishedPercentComplete ) && progressPayload . markAsFinishedPercentComplete > 0 ) {
2024-10-25 00:19:51 +02:00
const markAsFinishedPercentComplete = Number ( progressPayload . markAsFinishedPercentComplete ) / 100
shouldMarkAsFinished = markAsFinishedPercentComplete < this . progress
2024-10-22 00:48:02 +02:00
if ( shouldMarkAsFinished ) {
2024-10-25 00:19:51 +02:00
Logger . debug ( ` [MediaProgress] Marking media progress as finished because progress ( ${ this . progress } ) is greater than ${ markAsFinishedPercentComplete } ` )
2024-10-22 00:48:02 +02:00
}
} else {
2024-10-25 00:19:51 +02:00
const markAsFinishedTimeRemaining = isNullOrNaN ( progressPayload . markAsFinishedTimeRemaining ) ? 10 : Number ( progressPayload . markAsFinishedTimeRemaining )
shouldMarkAsFinished = timeRemaining < markAsFinishedTimeRemaining
2024-10-22 00:48:02 +02:00
if ( shouldMarkAsFinished ) {
Logger . debug ( ` [MediaProgress] Marking media progress as finished because time remaining ( ${ timeRemaining } ) is less than ${ markAsFinishedTimeRemaining } seconds ` )
}
}
}
2024-10-26 00:27:50 +02:00
if ( ! this . isFinished && shouldMarkAsFinished ) {
2024-08-11 18:53:30 +02:00
this . isFinished = true
this . finishedAt = this . finishedAt || Date . now ( )
this . extraData . progress = 1
this . changed ( 'extraData' , true )
2024-10-26 00:27:50 +02:00
} else if ( this . isFinished && this . changed ( 'currentTime' ) && ! shouldMarkAsFinished ) {
2024-08-11 18:53:30 +02:00
this . isFinished = false
this . finishedAt = null
}
// For local sync
if ( progressPayload . lastUpdate ) {
this . updatedAt = progressPayload . lastUpdate
}
return this . save ( )
}
2023-08-16 23:38:48 +02:00
}
2023-07-05 01:14:44 +02:00
2024-05-29 00:24:02 +02:00
module . exports = MediaProgress