mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-08 00:08:14 +01:00
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:
commit
de7296eaab
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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})`)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user