Merge pull request #3785 from advplyr/playback-session-use-new-library-item

Update PlaybackSession to use new library item model
This commit is contained in:
advplyr 2025-01-03 11:20:33 -06:00 committed by GitHub
commit de7296eaab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 284 additions and 193 deletions

View File

@ -149,7 +149,7 @@ class SessionController {
* @param {Response} res * @param {Response} res
*/ */
async getOpenSession(req, res) { async getOpenSession(req, res) {
const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) const libraryItem = await Database.libraryItemModel.getExpandedById(req.playbackSession.libraryItemId)
const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) const sessionForClient = req.playbackSession.toJSONForClient(libraryItem)
res.json(sessionForClient) res.json(sessionForClient)
} }

View File

@ -70,14 +70,13 @@ class ShareController {
} }
try { try {
const oldLibraryItem = await Database.mediaItemShareModel.getMediaItemsOldLibraryItem(mediaItemShare.mediaItemId, mediaItemShare.mediaItemType) const libraryItem = await Database.mediaItemShareModel.getMediaItemsLibraryItem(mediaItemShare.mediaItemId, mediaItemShare.mediaItemType)
if (!libraryItem) {
if (!oldLibraryItem) {
return res.status(404).send('Media item not found') return res.status(404).send('Media item not found')
} }
let startOffset = 0 let startOffset = 0
const publicTracks = oldLibraryItem.media.includedAudioFiles.map((audioFile) => { const publicTracks = libraryItem.media.includedAudioFiles.map((audioFile) => {
const audioTrack = { const audioTrack = {
index: audioFile.index, index: audioFile.index,
startOffset, startOffset,
@ -86,7 +85,7 @@ class ShareController {
contentUrl: `${global.RouterBasePath}/public/share/${slug}/track/${audioFile.index}`, contentUrl: `${global.RouterBasePath}/public/share/${slug}/track/${audioFile.index}`,
mimeType: audioFile.mimeType, mimeType: audioFile.mimeType,
codec: audioFile.codec || null, codec: audioFile.codec || null,
metadata: audioFile.metadata.clone() metadata: structuredClone(audioFile.metadata)
} }
startOffset += audioTrack.duration startOffset += audioTrack.duration
return audioTrack return audioTrack
@ -105,12 +104,12 @@ class ShareController {
const deviceInfo = await this.playbackSessionManager.getDeviceInfo(req, clientDeviceInfo) const deviceInfo = await this.playbackSessionManager.getDeviceInfo(req, clientDeviceInfo)
const newPlaybackSession = new PlaybackSession() const newPlaybackSession = new PlaybackSession()
newPlaybackSession.setData(oldLibraryItem, null, 'web-share', deviceInfo, startTime) newPlaybackSession.setData(libraryItem, null, 'web-share', deviceInfo, startTime)
newPlaybackSession.audioTracks = publicTracks newPlaybackSession.audioTracks = publicTracks
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
newPlaybackSession.shareSessionId = shareSessionId newPlaybackSession.shareSessionId = shareSessionId
newPlaybackSession.mediaItemShareId = mediaItemShare.id newPlaybackSession.mediaItemShareId = mediaItemShare.id
newPlaybackSession.coverAspectRatio = oldLibraryItem.librarySettings.coverAspectRatio newPlaybackSession.coverAspectRatio = libraryItem.library.settings.coverAspectRatio
mediaItemShare.playbackSession = newPlaybackSession.toJSONForClient() mediaItemShare.playbackSession = newPlaybackSession.toJSONForClient()
ShareManager.addOpenSharePlaybackSession(newPlaybackSession) ShareManager.addOpenSharePlaybackSession(newPlaybackSession)

View File

@ -74,7 +74,7 @@ class PlaybackSessionManager {
async startSessionRequest(req, res, episodeId) { async startSessionRequest(req, res, episodeId) {
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
const { oldLibraryItem: libraryItem, body: options } = req const { libraryItem, body: options } = req
const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options) const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options)
res.json(session.toJSONForClient(libraryItem)) res.json(session.toJSONForClient(libraryItem))
} }
@ -279,7 +279,7 @@ class PlaybackSessionManager {
* *
* @param {import('../models/User')} user * @param {import('../models/User')} user
* @param {DeviceInfo} deviceInfo * @param {DeviceInfo} deviceInfo
* @param {import('../objects/LibraryItem')} libraryItem * @param {import('../models/LibraryItem')} libraryItem
* @param {string|null} episodeId * @param {string|null} episodeId
* @param {{forceDirectPlay?:boolean, forceTranscode?:boolean, mediaPlayer:string, supportedMimeTypes?:string[]}} options * @param {{forceDirectPlay?:boolean, forceTranscode?:boolean, mediaPlayer:string, supportedMimeTypes?:string[]}} options
* @returns {Promise<PlaybackSession>} * @returns {Promise<PlaybackSession>}
@ -292,7 +292,7 @@ class PlaybackSessionManager {
await this.closeSession(user, session, null) await this.closeSession(user, session, null)
} }
const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId)) const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options.supportedMimeTypes, episodeId))
const mediaPlayer = options.mediaPlayer || 'unknown' const mediaPlayer = options.mediaPlayer || 'unknown'
const mediaItemId = episodeId || libraryItem.media.id const mediaItemId = episodeId || libraryItem.media.id
@ -300,7 +300,7 @@ class PlaybackSessionManager {
let userStartTime = 0 let userStartTime = 0
if (userProgress) { if (userProgress) {
if (userProgress.isFinished) { if (userProgress.isFinished) {
Logger.info(`[PlaybackSessionManager] Starting session for user "${user.username}" and resetting progress for finished item "${libraryItem.media.metadata.title}"`) Logger.info(`[PlaybackSessionManager] Starting session for user "${user.username}" and resetting progress for finished item "${libraryItem.media.title}"`)
// Keep userStartTime as 0 so the client restarts the media // Keep userStartTime as 0 so the client restarts the media
} else { } else {
userStartTime = Number.parseFloat(userProgress.currentTime) || 0 userStartTime = Number.parseFloat(userProgress.currentTime) || 0
@ -312,7 +312,7 @@ class PlaybackSessionManager {
let audioTracks = [] let audioTracks = []
if (shouldDirectPlay) { if (shouldDirectPlay) {
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`) Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`)
audioTracks = libraryItem.getDirectPlayTracklist(episodeId) audioTracks = libraryItem.getTrackList(episodeId)
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else { } else {
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`) Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`)

View File

@ -62,6 +62,13 @@ const parseNameString = require('../utils/parsers/parseNameString')
* @property {ChapterObject[]} chapters * @property {ChapterObject[]} chapters
* @property {Object} metaTags * @property {Object} metaTags
* @property {string} mimeType * @property {string} mimeType
*
* @typedef AudioTrackProperties
* @property {string} title
* @property {string} contentUrl
* @property {number} startOffset
*
* @typedef {AudioFileObject & AudioTrackProperties} AudioTrack
*/ */
class Book extends Model { class Book extends Model {
@ -367,16 +374,6 @@ class Book extends Model {
return this.audioFiles.filter((af) => !af.exclude) return this.audioFiles.filter((af) => !af.exclude)
} }
get trackList() {
let startOffset = 0
return this.includedAudioFiles.map((af) => {
const track = structuredClone(af)
track.startOffset = startOffset
startOffset += track.duration
return track
})
}
get hasMediaFiles() { get hasMediaFiles() {
return !!this.hasAudioTracks || !!this.ebookFile return !!this.hasAudioTracks || !!this.ebookFile
} }
@ -385,6 +382,59 @@ class Book extends Model {
return !!this.includedAudioFiles.length return !!this.includedAudioFiles.length
} }
/**
* Supported mime types are sent from the web client and are retrieved using the browser Audio player "canPlayType" function.
*
* @param {string[]} supportedMimeTypes
* @returns {boolean}
*/
checkCanDirectPlay(supportedMimeTypes) {
if (!Array.isArray(supportedMimeTypes)) {
Logger.error(`[Book] checkCanDirectPlay: supportedMimeTypes is not an array`, supportedMimeTypes)
return false
}
return this.includedAudioFiles.every((af) => supportedMimeTypes.includes(af.mimeType))
}
/**
* Get the track list to be used in client audio players
* AudioTrack is the AudioFile with startOffset, contentUrl and title
*
* @param {string} libraryItemId
* @returns {AudioTrack[]}
*/
getTracklist(libraryItemId) {
let startOffset = 0
return this.includedAudioFiles.map((af) => {
const track = structuredClone(af)
track.title = af.metadata.filename
track.startOffset = startOffset
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
startOffset += track.duration
return track
})
}
/**
*
* @returns {ChapterObject[]}
*/
getChapters() {
return structuredClone(this.chapters) || []
}
getPlaybackTitle() {
return this.title
}
getPlaybackAuthor() {
return this.authorName
}
getPlaybackDuration() {
return this.duration
}
/** /**
* Total file size of all audio files and ebook file * Total file size of all audio files and ebook file
* *
@ -635,7 +685,7 @@ class Book extends Model {
metadata: this.oldMetadataToJSONMinified(), metadata: this.oldMetadataToJSONMinified(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...(this.tags || [])], tags: [...(this.tags || [])],
numTracks: this.trackList.length, numTracks: this.includedAudioFiles.length,
numAudioFiles: this.audioFiles?.length || 0, numAudioFiles: this.audioFiles?.length || 0,
numChapters: this.chapters?.length || 0, numChapters: this.chapters?.length || 0,
duration: this.duration, duration: this.duration,
@ -666,7 +716,7 @@ class Book extends Model {
ebookFile: structuredClone(this.ebookFile), ebookFile: structuredClone(this.ebookFile),
duration: this.duration, duration: this.duration,
size: this.size, size: this.size,
tracks: structuredClone(this.trackList) tracks: this.getTracklist(libraryItemId)
} }
} }
} }

View File

@ -112,15 +112,15 @@ class FeedEpisode extends Model {
/** /**
* If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names * If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names
* *
* @param {import('./Book').AudioTrack[]} trackList
* @param {import('./Book')} book * @param {import('./Book')} book
* @returns {boolean} * @returns {boolean}
*/ */
static checkUseChapterTitlesForEpisodes(book) { static checkUseChapterTitlesForEpisodes(trackList, book) {
const tracks = book.trackList || []
const chapters = book.chapters || [] const chapters = book.chapters || []
if (tracks.length !== chapters.length) return false if (trackList.length !== chapters.length) return false
for (let i = 0; i < tracks.length; i++) { for (let i = 0; i < trackList.length; i++) {
if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) { if (Math.abs(chapters[i].start - trackList[i].startOffset) >= 1) {
return false return false
} }
} }
@ -148,7 +148,7 @@ class FeedEpisode extends Model {
const contentUrl = `/feed/${slug}/item/${episodeId}/media${Path.extname(audioTrack.metadata.filename)}` const contentUrl = `/feed/${slug}/item/${episodeId}/media${Path.extname(audioTrack.metadata.filename)}`
let title = Path.basename(audioTrack.metadata.filename, Path.extname(audioTrack.metadata.filename)) let title = Path.basename(audioTrack.metadata.filename, Path.extname(audioTrack.metadata.filename))
if (book.trackList.length == 1) { if (book.includedAudioFiles.length == 1) {
// If audiobook is a single file, use book title instead of chapter/file title // If audiobook is a single file, use book title instead of chapter/file title
title = book.title title = book.title
} else { } else {
@ -185,11 +185,12 @@ class FeedEpisode extends Model {
* @returns {Promise<FeedEpisode[]>} * @returns {Promise<FeedEpisode[]>}
*/ */
static async createFromAudiobookTracks(libraryItemExpanded, feed, slug, transaction) { static async createFromAudiobookTracks(libraryItemExpanded, feed, slug, transaction) {
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItemExpanded.media) const trackList = libraryItemExpanded.getTrackList()
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(trackList, libraryItemExpanded.media)
const feedEpisodeObjs = [] const feedEpisodeObjs = []
let numExisting = 0 let numExisting = 0
for (const track of libraryItemExpanded.media.trackList) { for (const track of trackList) {
// Check for existing episode by filepath // Check for existing episode by filepath
const existingEpisode = feed.feedEpisodes?.find((episode) => { const existingEpisode = feed.feedEpisodes?.find((episode) => {
return episode.filePath === track.metadata.path return episode.filePath === track.metadata.path
@ -204,7 +205,7 @@ class FeedEpisode extends Model {
/** /**
* *
* @param {import('./Book')[]} books * @param {import('./Book').BookExpandedWithLibraryItem[]} books
* @param {import('./Feed')} feed * @param {import('./Feed')} feed
* @param {string} slug * @param {string} slug
* @param {import('sequelize').Transaction} transaction * @param {import('sequelize').Transaction} transaction
@ -218,8 +219,9 @@ class FeedEpisode extends Model {
const feedEpisodeObjs = [] const feedEpisodeObjs = []
let numExisting = 0 let numExisting = 0
for (const book of books) { for (const book of books) {
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(book) const trackList = book.libraryItem.getTrackList()
for (const track of book.trackList) { const useChapterTitles = this.checkUseChapterTitlesForEpisodes(trackList, book)
for (const track of trackList) {
// Check for existing episode by filepath // Check for existing episode by filepath
const existingEpisode = feed.feedEpisodes?.find((episode) => { const existingEpisode = feed.feedEpisodes?.find((episode) => {
return episode.filePath === track.metadata.path return episode.filePath === track.metadata.path

View File

@ -497,6 +497,57 @@ class LibraryItem extends Model {
return libraryItem return libraryItem
} }
/**
*
* @param {import('sequelize').WhereOptions} where
* @param {import('sequelize').IncludeOptions} [include]
* @returns {Promise<LibraryItemExpanded>}
*/
static async findOneExpanded(where, include = null) {
const libraryItem = await this.findOne({
where,
include
})
if (!libraryItem) {
Logger.error(`[LibraryItem] Library item not found`)
return null
}
if (libraryItem.mediaType === 'podcast') {
libraryItem.media = await libraryItem.getMedia({
include: [
{
model: this.sequelize.models.podcastEpisode
}
]
})
} else {
libraryItem.media = await libraryItem.getMedia({
include: [
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['id', 'sequence']
}
}
],
order: [
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
]
})
}
if (!libraryItem.media) return null
return libraryItem
}
/** /**
* Get old library item by id * Get old library item by id
* @param {string} libraryItemId * @param {string} libraryItemId
@ -1176,6 +1227,22 @@ class LibraryItem extends Model {
} }
} }
/**
* Get the track list to be used in client audio players
* AudioTrack is the AudioFile with startOffset and contentUrl
* Podcasts must have an episodeId to get the track list
*
* @param {string} [episodeId]
* @returns {import('./Book').AudioTrack[]}
*/
getTrackList(episodeId) {
if (!this.media) {
Logger.error(`[LibraryItem] getTrackList: Library item "${this.id}" does not have media`)
return []
}
return this.media.getTracklist(this.id, episodeId)
}
/** /**
* *
* @param {string} ino * @param {string} ino

View File

@ -76,42 +76,26 @@ class MediaItemShare extends Model {
} }
/** /**
* Expanded book that includes library settings
* *
* @param {string} mediaItemId * @param {string} mediaItemId
* @param {string} mediaItemType * @param {string} mediaItemType
* @returns {Promise<import('../objects/LibraryItem')>} * @returns {Promise<import('./LibraryItem').LibraryItemExpanded>}
*/ */
static async getMediaItemsOldLibraryItem(mediaItemId, mediaItemType) { static async getMediaItemsLibraryItem(mediaItemId, mediaItemType) {
/** @type {typeof import('./LibraryItem')} */
const libraryItemModel = this.sequelize.models.libraryItem
if (mediaItemType === 'book') { if (mediaItemType === 'book') {
const book = await this.sequelize.models.book.findByPk(mediaItemId, { const libraryItem = await libraryItemModel.findOneExpanded(
include: [ { mediaId: mediaItemId },
{ {
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
},
{
model: this.sequelize.models.libraryItem,
include: {
model: this.sequelize.models.library, model: this.sequelize.models.library,
attributes: ['settings'] attributes: ['settings']
} }
} )
]
}) return libraryItem
const libraryItem = book.libraryItem
libraryItem.media = book
delete book.libraryItem
const oldLibraryItem = this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
oldLibraryItem.librarySettings = libraryItem.library.settings
return oldLibraryItem
} }
return null return null
} }

View File

@ -276,6 +276,78 @@ class Podcast extends Model {
return hasUpdates return hasUpdates
} }
checkCanDirectPlay(supportedMimeTypes, episodeId) {
if (!Array.isArray(supportedMimeTypes)) {
Logger.error(`[Podcast] checkCanDirectPlay: supportedMimeTypes is not an array`, supportedMimeTypes)
return false
}
const episode = this.podcastEpisodes.find((ep) => ep.id === episodeId)
if (!episode) {
Logger.error(`[Podcast] checkCanDirectPlay: episode not found`, episodeId)
return false
}
return supportedMimeTypes.includes(episode.audioFile.mimeType)
}
/**
* Get the track list to be used in client audio players
* AudioTrack is the AudioFile with startOffset and contentUrl
* Podcast episodes only have one track
*
* @param {string} libraryItemId
* @param {string} episodeId
* @returns {import('./Book').AudioTrack[]}
*/
getTracklist(libraryItemId, episodeId) {
const episode = this.podcastEpisodes.find((ep) => ep.id === episodeId)
if (!episode) {
Logger.error(`[Podcast] getTracklist: episode not found`, episodeId)
return []
}
const audioTrack = episode.getAudioTrack(libraryItemId)
return [audioTrack]
}
/**
*
* @param {string} episodeId
* @returns {import('./PodcastEpisode').ChapterObject[]}
*/
getChapters(episodeId) {
const episode = this.podcastEpisodes.find((ep) => ep.id === episodeId)
if (!episode) {
Logger.error(`[Podcast] getChapters: episode not found`, episodeId)
return []
}
return structuredClone(episode.chapters) || []
}
getPlaybackTitle(episodeId) {
const episode = this.podcastEpisodes.find((ep) => ep.id === episodeId)
if (!episode) {
Logger.error(`[Podcast] getPlaybackTitle: episode not found`, episodeId)
return ''
}
return episode.title
}
getPlaybackAuthor() {
return this.author
}
getPlaybackDuration(episodeId) {
const episode = this.podcastEpisodes.find((ep) => ep.id === episodeId)
if (!episode) {
Logger.error(`[Podcast] getPlaybackDuration: episode not found`, episodeId)
return 0
}
return episode.duration
}
/** /**
* Old model kept metadata in a separate object * Old model kept metadata in a separate object
*/ */

View File

@ -135,23 +135,28 @@ class PodcastEpisode extends Model {
PodcastEpisode.belongsTo(podcast) PodcastEpisode.belongsTo(podcast)
} }
get size() {
return this.audioFile?.metadata.size || 0
}
get duration() {
return this.audioFile?.duration || 0
}
/** /**
* AudioTrack object used in old model * Used in client players
* *
* @returns {import('./Book').AudioFileObject|null} * @param {string} libraryItemId
* @returns {import('./Book').AudioTrack}
*/ */
get track() { getAudioTrack(libraryItemId) {
if (!this.audioFile) return null
const track = structuredClone(this.audioFile) const track = structuredClone(this.audioFile)
track.startOffset = 0 track.startOffset = 0
track.title = this.audioFile.metadata.title track.title = this.audioFile.metadata.title
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
return track return track
} }
get size() {
return this.audioFile?.metadata.size || 0
}
/** /**
* @param {string} libraryItemId * @param {string} libraryItemId
* @returns {oldPodcastEpisode} * @returns {oldPodcastEpisode}
@ -228,9 +233,9 @@ class PodcastEpisode extends Model {
toOldJSONExpanded(libraryItemId) { toOldJSONExpanded(libraryItemId) {
const json = this.toOldJSON(libraryItemId) const json = this.toOldJSON(libraryItemId)
json.audioTrack = this.track json.audioTrack = this.getAudioTrack(libraryItemId)
json.size = this.size json.size = this.size
json.duration = this.audioFile?.duration || 0 json.duration = this.duration
return json return json
} }

View File

@ -249,10 +249,6 @@ class LibraryItem {
this.updatedAt = Date.now() this.updatedAt = Date.now()
} }
getDirectPlayTracklist(episodeId) {
return this.media.getDirectPlayTracklist(episodeId)
}
/** /**
* Save metadata.json file * Save metadata.json file
* TODO: Move to new LibraryItem model * TODO: Move to new LibraryItem model

View File

@ -1,8 +1,6 @@
const date = require('../libs/dateAndTime') const date = require('../libs/dateAndTime')
const uuidv4 = require('uuid').v4 const uuidv4 = require('uuid').v4
const serverVersion = require('../../package.json').version const serverVersion = require('../../package.json').version
const BookMetadata = require('./metadata/BookMetadata')
const PodcastMetadata = require('./metadata/PodcastMetadata')
const DeviceInfo = require('./DeviceInfo') const DeviceInfo = require('./DeviceInfo')
class PlaybackSession { class PlaybackSession {
@ -60,7 +58,7 @@ class PlaybackSession {
bookId: this.bookId, bookId: this.bookId,
episodeId: this.episodeId, episodeId: this.episodeId,
mediaType: this.mediaType, mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata?.toJSON() || null, mediaMetadata: structuredClone(this.mediaMetadata),
chapters: (this.chapters || []).map((c) => ({ ...c })), chapters: (this.chapters || []).map((c) => ({ ...c })),
displayTitle: this.displayTitle, displayTitle: this.displayTitle,
displayAuthor: this.displayAuthor, displayAuthor: this.displayAuthor,
@ -82,7 +80,7 @@ class PlaybackSession {
/** /**
* Session data to send to clients * Session data to send to clients
* @param {Object} [libraryItem] - old library item * @param {import('../models/LibraryItem')} [libraryItem]
* @returns * @returns
*/ */
toJSONForClient(libraryItem) { toJSONForClient(libraryItem) {
@ -94,7 +92,7 @@ class PlaybackSession {
bookId: this.bookId, bookId: this.bookId,
episodeId: this.episodeId, episodeId: this.episodeId,
mediaType: this.mediaType, mediaType: this.mediaType,
mediaMetadata: this.mediaMetadata?.toJSON() || null, mediaMetadata: structuredClone(this.mediaMetadata),
chapters: (this.chapters || []).map((c) => ({ ...c })), chapters: (this.chapters || []).map((c) => ({ ...c })),
displayTitle: this.displayTitle, displayTitle: this.displayTitle,
displayAuthor: this.displayAuthor, displayAuthor: this.displayAuthor,
@ -112,7 +110,7 @@ class PlaybackSession {
startedAt: this.startedAt, startedAt: this.startedAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }), audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
libraryItem: libraryItem?.toJSONExpanded() || null libraryItem: libraryItem?.toOldJSONExpanded() || null
} }
} }
@ -148,14 +146,7 @@ class PlaybackSession {
this.serverVersion = session.serverVersion this.serverVersion = session.serverVersion
this.chapters = session.chapters || [] this.chapters = session.chapters || []
this.mediaMetadata = null this.mediaMetadata = session.mediaMetadata
if (session.mediaMetadata) {
if (this.mediaType === 'book') {
this.mediaMetadata = new BookMetadata(session.mediaMetadata)
} else if (this.mediaType === 'podcast') {
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
}
}
this.displayTitle = session.displayTitle || '' this.displayTitle = session.displayTitle || ''
this.displayAuthor = session.displayAuthor || '' this.displayAuthor = session.displayAuthor || ''
this.coverPath = session.coverPath this.coverPath = session.coverPath
@ -205,6 +196,15 @@ class PlaybackSession {
} }
} }
/**
*
* @param {import('../models/LibraryItem')} libraryItem
* @param {*} userId
* @param {*} mediaPlayer
* @param {*} deviceInfo
* @param {*} startTime
* @param {*} episodeId
*/
setData(libraryItem, userId, mediaPlayer, deviceInfo, startTime, episodeId = null) { setData(libraryItem, userId, mediaPlayer, deviceInfo, startTime, episodeId = null) {
this.id = uuidv4() this.id = uuidv4()
this.userId = userId this.userId = userId
@ -213,13 +213,12 @@ class PlaybackSession {
this.bookId = episodeId ? null : libraryItem.media.id this.bookId = episodeId ? null : libraryItem.media.id
this.episodeId = episodeId this.episodeId = episodeId
this.mediaType = libraryItem.mediaType this.mediaType = libraryItem.mediaType
this.mediaMetadata = libraryItem.media.metadata.clone() this.mediaMetadata = libraryItem.media.oldMetadataToJSON()
this.chapters = libraryItem.media.getChapters(episodeId) this.chapters = libraryItem.media.getChapters(episodeId)
this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId) this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId)
this.displayAuthor = libraryItem.media.getPlaybackAuthor() this.displayAuthor = libraryItem.media.getPlaybackAuthor()
this.coverPath = libraryItem.media.coverPath this.coverPath = libraryItem.media.coverPath
this.duration = libraryItem.media.getPlaybackDuration(episodeId)
this.setDuration(libraryItem, episodeId)
this.mediaPlayer = mediaPlayer this.mediaPlayer = mediaPlayer
this.deviceInfo = deviceInfo || new DeviceInfo() this.deviceInfo = deviceInfo || new DeviceInfo()
@ -235,14 +234,6 @@ class PlaybackSession {
this.updatedAt = Date.now() this.updatedAt = Date.now()
} }
setDuration(libraryItem, episodeId) {
if (episodeId) {
this.duration = libraryItem.media.getEpisodeDuration(episodeId)
} else {
this.duration = libraryItem.media.duration
}
}
addListeningTime(timeListened) { addListeningTime(timeListened) {
if (!timeListened || isNaN(timeListened)) return if (!timeListened || isNaN(timeListened)) return

View File

@ -18,6 +18,7 @@ class Stream extends EventEmitter {
this.id = sessionId this.id = sessionId
this.user = user this.user = user
/** @type {import('../models/LibraryItem')} */
this.libraryItem = libraryItem this.libraryItem = libraryItem
this.episodeId = episodeId this.episodeId = episodeId
@ -40,31 +41,25 @@ class Stream extends EventEmitter {
this.furthestSegmentCreated = 0 this.furthestSegmentCreated = 0
} }
get isPodcast() { /**
return this.libraryItem.mediaType === 'podcast' * @returns {import('../models/PodcastEpisode') | null}
} */
get episode() { get episode() {
if (!this.isPodcast) return null if (!this.libraryItem.isPodcast) return null
return this.libraryItem.media.episodes.find((ep) => ep.id === this.episodeId) return this.libraryItem.media.podcastEpisodes.find((ep) => ep.id === this.episodeId)
}
get libraryItemId() {
return this.libraryItem.id
} }
get mediaTitle() { get mediaTitle() {
if (this.episode) return this.episode.title || '' return this.libraryItem.media.getPlaybackTitle(this.episodeId)
return this.libraryItem.media.metadata.title || ''
} }
get totalDuration() { get totalDuration() {
if (this.episode) return this.episode.duration return this.libraryItem.media.getPlaybackDuration(this.episodeId)
return this.libraryItem.media.duration
} }
get tracks() { get tracks() {
if (this.episode) return this.episode.tracks return this.libraryItem.getTrackList(this.episodeId)
return this.libraryItem.media.tracks
} }
get tracksAudioFileType() { get tracksAudioFileType() {
if (!this.tracks.length) return null if (!this.tracks.length) return null
return this.tracks[0].metadata.format return this.tracks[0].metadata.ext.slice(1)
} }
get tracksMimeType() { get tracksMimeType() {
if (!this.tracks.length) return null if (!this.tracks.length) return null
@ -116,8 +111,8 @@ class Stream extends EventEmitter {
return { return {
id: this.id, id: this.id,
userId: this.user.id, userId: this.user.id,
libraryItem: this.libraryItem.toJSONExpanded(), libraryItem: this.libraryItem.toOldJSONExpanded(),
episode: this.episode ? this.episode.toJSONExpanded() : null, episode: this.episode ? this.episode.toOldJSONExpanded(this.libraryItem.id) : null,
segmentLength: this.segmentLength, segmentLength: this.segmentLength,
playlistPath: this.playlistPath, playlistPath: this.playlistPath,
clientPlaylistUri: this.clientPlaylistUri, clientPlaylistUri: this.clientPlaylistUri,

View File

@ -168,16 +168,6 @@ class PodcastEpisode {
return hasUpdates return hasUpdates
} }
// Only checks container format
checkCanDirectPlay(payload) {
const supportedMimeTypes = payload.supportedMimeTypes || []
return supportedMimeTypes.includes(this.audioFile.mimeType)
}
getDirectPlayTracklist() {
return this.tracks
}
checkEqualsEnclosureUrl(url) { checkEqualsEnclosureUrl(url) {
if (!this.enclosure?.url) return false if (!this.enclosure?.url) return false
return this.enclosure.url == url return this.enclosure.url == url

View File

@ -150,27 +150,5 @@ class Book {
this.coverPath = coverPath this.coverPath = coverPath
return true return true
} }
// Only checks container format
checkCanDirectPlay(payload) {
var supportedMimeTypes = payload.supportedMimeTypes || []
return !this.tracks.some((t) => !supportedMimeTypes.includes(t.mimeType))
}
getDirectPlayTracklist() {
return this.tracks
}
getPlaybackTitle() {
return this.metadata.title
}
getPlaybackAuthor() {
return this.metadata.authorName
}
getChapters() {
return this.chapters?.map((ch) => ({ ...ch })) || []
}
} }
module.exports = Book module.exports = Book

View File

@ -199,19 +199,6 @@ class Podcast {
return this.episodes.some((ep) => (ep.guid && ep.guid === guid) || ep.checkEqualsEnclosureUrl(url)) return this.episodes.some((ep) => (ep.guid && ep.guid === guid) || ep.checkEqualsEnclosureUrl(url))
} }
// Only checks container format
checkCanDirectPlay(payload, episodeId) {
var episode = this.episodes.find((ep) => ep.id === episodeId)
if (!episode) return false
return episode.checkCanDirectPlay(payload)
}
getDirectPlayTracklist(episodeId) {
var episode = this.episodes.find((ep) => ep.id === episodeId)
if (!episode) return false
return episode.getDirectPlayTracklist()
}
addPodcastEpisode(podcastEpisode) { addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode) this.episodes.push(podcastEpisode)
} }
@ -224,22 +211,6 @@ class Podcast {
return episode return episode
} }
getPlaybackTitle(episodeId) {
var episode = this.episodes.find((ep) => ep.id == episodeId)
if (!episode) return this.metadata.title
return episode.title
}
getPlaybackAuthor() {
return this.metadata.author
}
getEpisodeDuration(episodeId) {
var episode = this.episodes.find((ep) => ep.id == episodeId)
if (!episode) return 0
return episode.duration
}
getEpisode(episodeId) { getEpisode(episodeId) {
if (!episodeId) return null if (!episodeId) return null
@ -248,9 +219,5 @@ class Podcast {
return this.episodes.find((ep) => ep.id == episodeId) return this.episodes.find((ep) => ep.id == episodeId)
} }
getChapters(episodeId) {
return this.getEpisode(episodeId)?.chapters?.map((ch) => ({ ...ch })) || []
}
} }
module.exports = Podcast module.exports = Podcast

View File

@ -159,11 +159,6 @@ class BookMetadata {
getSeries(seriesId) { getSeries(seriesId) {
return this.series.find((se) => se.id == seriesId) return this.series.find((se) => se.id == seriesId)
} }
getSeriesSequence(seriesId) {
const series = this.series.find((se) => se.id == seriesId)
if (!series) return null
return series.sequence || ''
}
update(payload) { update(payload) {
const json = this.toJSON() const json = this.toJSON()