mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-01 08:44:40 +02:00
Add playing podcast episodes, episode progress, podcast page, podcast home page shelves
This commit is contained in:
parent
e32d05ea27
commit
0e665e2091
28 changed files with 526 additions and 82 deletions
|
@ -384,6 +384,10 @@ class Server {
|
|||
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
||||
this.playbackSessionManager.removeSession(session.id)
|
||||
session = null
|
||||
} else if (session.mediaType === 'podcast' && !sessionLibraryItem.media.checkHasEpisode(session.episodeId)) {
|
||||
Logger.error(`[Server] Library Item for session "${session.id}" episode ${session.episodeId} does not exist "${session.libraryItemId}"`)
|
||||
this.playbackSessionManager.removeSession(session.id)
|
||||
session = null
|
||||
}
|
||||
if (session) {
|
||||
session = session.toJSONForClient(sessionLibraryItem)
|
||||
|
|
|
@ -275,6 +275,8 @@ class LibraryController {
|
|||
|
||||
// api/libraries/:id/personalized
|
||||
async getLibraryUserPersonalized(req, res) {
|
||||
var mediaType = req.library.mediaType
|
||||
var isPodcastLibrary = mediaType == 'podcast'
|
||||
var libraryItems = req.libraryItems
|
||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
var minified = req.query.minified === '1'
|
||||
|
@ -283,8 +285,8 @@ class LibraryController {
|
|||
|
||||
var categories = [
|
||||
{
|
||||
id: 'continue-reading',
|
||||
label: 'Continue Reading',
|
||||
id: 'continue-listening',
|
||||
label: 'Continue Listening',
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
||||
},
|
||||
|
@ -295,8 +297,8 @@ class LibraryController {
|
|||
entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||
},
|
||||
{
|
||||
id: 'read-again',
|
||||
label: 'Read Again',
|
||||
id: 'listen-again',
|
||||
label: 'Listen Again',
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
||||
}
|
||||
|
|
|
@ -169,7 +169,24 @@ class LibraryItemController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
const options = req.body || {}
|
||||
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, options, res)
|
||||
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, null, options, res)
|
||||
}
|
||||
|
||||
// POST: api/items/:id/play/:episodeId
|
||||
startEpisodePlaybackSession(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
if (!libraryItem.media.numTracks) {
|
||||
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
var episodeId = req.params.episodeId
|
||||
if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) {
|
||||
Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const options = req.body || {}
|
||||
this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res)
|
||||
}
|
||||
|
||||
// PATCH: api/items/:id/tracks
|
||||
|
@ -186,6 +203,38 @@ class LibraryItemController {
|
|||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
// PATCH: api/items/:id/episodes
|
||||
async updateEpisodes(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
var orderedFileData = req.body.episodes
|
||||
if (!libraryItem.media.setEpisodeOrder) {
|
||||
Logger.error(`[LibraryItemController] updateEpisodes invalid media type ${libraryItem.id}`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
libraryItem.media.setEpisodeOrder(orderedFileData)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
// DELETE: api/items/:id/episode/:episodeId
|
||||
async removeEpisode(req, res) {
|
||||
var episodeId = req.params.episodeId
|
||||
var libraryItem = req.libraryItem
|
||||
if (!libraryItem.mediaType !== 'podcast') {
|
||||
Logger.error(`[LibraryItemController] removeEpisode invalid media type ${libraryItem.id}`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) {
|
||||
Logger.error(`[LibraryItemController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
libraryItem.media.removeEpisode(episodeId)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
// POST api/items/:id/match
|
||||
async match(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
|
|
|
@ -43,6 +43,26 @@ class MeController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// PATCH: api/me/progress/:id/:episodeId
|
||||
async createUpdateEpisodeMediaProgress(req, res) {
|
||||
var episodeId = req.params.episodeId
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) {
|
||||
Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.status(404).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// PATCH: api/me/progress/batch/update
|
||||
async batchUpdateMediaProgress(req, res) {
|
||||
var itemProgressPayloads = req.body
|
||||
|
|
|
@ -25,8 +25,8 @@ class PlaybackSessionManager {
|
|||
return session ? session.stream : null
|
||||
}
|
||||
|
||||
async startSessionRequest(user, libraryItem, options, res) {
|
||||
const session = await this.startSession(user, libraryItem, options)
|
||||
async startSessionRequest(user, libraryItem, episodeId, options, res) {
|
||||
const session = await this.startSession(user, libraryItem, episodeId, options)
|
||||
res.json(session.toJSONForClient(libraryItem))
|
||||
}
|
||||
|
||||
|
@ -42,23 +42,23 @@ class PlaybackSessionManager {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async startSession(user, libraryItem, options) {
|
||||
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options))
|
||||
async startSession(user, libraryItem, episodeId, options) {
|
||||
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
||||
|
||||
const userProgress = user.getMediaProgress(libraryItem.id)
|
||||
const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
|
||||
var userStartTime = 0
|
||||
if (userProgress) userStartTime = userProgress.currentTime || 0
|
||||
const newPlaybackSession = new PlaybackSession()
|
||||
newPlaybackSession.setData(libraryItem, user)
|
||||
newPlaybackSession.setData(libraryItem, user, episodeId)
|
||||
|
||||
var audioTracks = []
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id, episodeId)
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
} else {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, userStartTime, this.clientEmitter.bind(this))
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||
await stream.generatePlaylist()
|
||||
audioTracks = [stream.getAudioTrack()]
|
||||
newPlaybackSession.stream = stream
|
||||
|
@ -84,7 +84,7 @@ class PlaybackSessionManager {
|
|||
async syncSession(user, session, syncData) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${sessino.libraryItemId}"`)
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -97,10 +97,11 @@ class PlaybackSessionManager {
|
|||
currentTime: syncData.currentTime,
|
||||
progress: session.progress
|
||||
}
|
||||
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate)
|
||||
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
||||
if (wasUpdated) {
|
||||
|
||||
await this.db.updateEntity('user', user)
|
||||
var itemProgress = user.getMediaProgress(session.libraryItemId)
|
||||
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||
id: itemProgress.id,
|
||||
data: itemProgress.toJSON()
|
||||
|
|
|
@ -440,8 +440,8 @@ class LibraryItem {
|
|||
return this.media.searchQuery(query)
|
||||
}
|
||||
|
||||
getDirectPlayTracklist(libraryItemId) {
|
||||
return this.media.getDirectPlayTracklist(libraryItemId)
|
||||
getDirectPlayTracklist(libraryItemId, episodeId) {
|
||||
return this.media.getDirectPlayTracklist(libraryItemId, episodeId)
|
||||
}
|
||||
}
|
||||
module.exports = LibraryItem
|
|
@ -9,6 +9,7 @@ class PlaybackSession {
|
|||
this.id = null
|
||||
this.userId = null
|
||||
this.libraryItemId = null
|
||||
this.episodeId = null
|
||||
|
||||
this.mediaType = null
|
||||
this.mediaMetadata = null
|
||||
|
@ -41,6 +42,7 @@ class PlaybackSession {
|
|||
sessionType: this.sessionType,
|
||||
userId: this.userId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
|
||||
coverPath: this.coverPath,
|
||||
|
@ -60,6 +62,7 @@ class PlaybackSession {
|
|||
sessionType: this.sessionType,
|
||||
userId: this.userId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
|
||||
coverPath: this.coverPath,
|
||||
|
@ -81,7 +84,8 @@ class PlaybackSession {
|
|||
this.sessionType = session.sessionType
|
||||
this.userId = session.userId
|
||||
this.libraryItemId = session.libraryItemId
|
||||
this.mediaType = session.mediaType
|
||||
this.episodeId = session.episodeId,
|
||||
this.mediaType = session.mediaType
|
||||
this.duration = session.duration
|
||||
this.playMethod = session.playMethod
|
||||
|
||||
|
@ -107,10 +111,11 @@ class PlaybackSession {
|
|||
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
|
||||
}
|
||||
|
||||
setData(libraryItem, user) {
|
||||
setData(libraryItem, user, episodeId = null) {
|
||||
this.id = getId('play')
|
||||
this.userId = user.id
|
||||
this.libraryItemId = libraryItem.id
|
||||
this.episodeId = episodeId
|
||||
this.mediaType = libraryItem.mediaType
|
||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||
this.coverPath = libraryItem.media.coverPath
|
||||
|
|
|
@ -9,12 +9,13 @@ const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
|||
const AudioTrack = require('./files/AudioTrack')
|
||||
|
||||
class Stream extends EventEmitter {
|
||||
constructor(sessionId, streamPath, user, libraryItem, startTime, clientEmitter, transcodeOptions = {}) {
|
||||
constructor(sessionId, streamPath, user, libraryItem, episodeId, startTime, clientEmitter, transcodeOptions = {}) {
|
||||
super()
|
||||
|
||||
this.id = sessionId
|
||||
this.user = user
|
||||
this.libraryItem = libraryItem
|
||||
this.episodeId = episodeId
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.transcodeOptions = transcodeOptions
|
||||
|
@ -34,22 +35,28 @@ class Stream extends EventEmitter {
|
|||
this.isTranscodeComplete = false
|
||||
this.segmentsCreated = new Set()
|
||||
this.furthestSegmentCreated = 0
|
||||
// this.clientCurrentTime = 0
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
get isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
}
|
||||
get episode() {
|
||||
if (!this.isPodcast) return null
|
||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
||||
}
|
||||
get libraryItemId() {
|
||||
return this.libraryItem.id
|
||||
}
|
||||
get mediaTitle() {
|
||||
if (this.episode) return this.episode.title || ''
|
||||
return this.libraryItem.media.metadata.title || ''
|
||||
}
|
||||
get totalDuration() {
|
||||
if (this.episode) return this.episode.duration
|
||||
return this.libraryItem.media.duration
|
||||
}
|
||||
get tracks() {
|
||||
// TODO: Podcast episode tracks
|
||||
if (this.episode) return this.episode.tracks
|
||||
return this.libraryItem.media.tracks
|
||||
}
|
||||
get tracksAudioFileType() {
|
||||
|
@ -99,28 +106,16 @@ class Stream extends EventEmitter {
|
|||
id: this.id,
|
||||
userId: this.user.id,
|
||||
libraryItem: this.libraryItem.toJSONExpanded(),
|
||||
episode: this.episode ? this.episode.toJSONExpanded() : null,
|
||||
segmentLength: this.segmentLength,
|
||||
playlistPath: this.playlistPath,
|
||||
clientPlaylistUri: this.clientPlaylistUri,
|
||||
// clientCurrentTime: this.clientCurrentTime,
|
||||
startTime: this.startTime,
|
||||
segmentStartNumber: this.segmentStartNumber,
|
||||
isTranscodeComplete: this.isTranscodeComplete,
|
||||
// lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// if (this.clientUserAudiobookData) {
|
||||
// var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
|
||||
// Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
|
||||
// if (timeRemaining > 15) {
|
||||
// this.startTime = this.clientUserAudiobookData.currentTime
|
||||
// this.clientCurrentTime = this.startTime
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
async checkSegmentNumberRequest(segNum) {
|
||||
var segStartTime = segNum * this.segmentLength
|
||||
if (this.startTime > segStartTime) {
|
||||
|
|
|
@ -143,14 +143,20 @@ class Podcast {
|
|||
return payload || {}
|
||||
}
|
||||
|
||||
checkHasEpisode(episodeId) {
|
||||
return this.episodes.some(ep => ep.id === episodeId)
|
||||
}
|
||||
|
||||
// Only checks container format
|
||||
checkCanDirectPlay(payload, epsiodeIndex = 0) {
|
||||
var episode = this.episodes[epsiodeIndex]
|
||||
checkCanDirectPlay(payload, episodeId) {
|
||||
var episode = this.episodes.find(ep => ep.id === episodeId)
|
||||
if (!episode) return false
|
||||
return episode.checkCanDirectPlay(payload)
|
||||
}
|
||||
|
||||
getDirectPlayTracklist(libraryItemId, episodeIndex = 0) {
|
||||
var episode = this.episodes[episodeIndex]
|
||||
getDirectPlayTracklist(libraryItemId, episodeId) {
|
||||
var episode = this.episodes.find(ep => ep.id === episodeId)
|
||||
if (!episode) return false
|
||||
return episode.getDirectPlayTracklist(libraryItemId)
|
||||
}
|
||||
|
||||
|
@ -164,6 +170,15 @@ class Podcast {
|
|||
this.episodes.push(pe)
|
||||
}
|
||||
|
||||
setEpisodeOrder(episodeIds) {
|
||||
this.episodes = this.episodes.map(ep => {
|
||||
var indexOf = episodeIds.findIndex(id => id === ep.id)
|
||||
ep.index = indexOf + 1
|
||||
return ep
|
||||
})
|
||||
this.episodes.sort((a, b) => b.index - a.index)
|
||||
}
|
||||
|
||||
reorderEpisodes() {
|
||||
var hasUpdates = false
|
||||
this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename)
|
||||
|
@ -173,7 +188,12 @@ class Podcast {
|
|||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
this.episodes.sort((a, b) => b.index - a.index)
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
removeEpisode(episodeId) {
|
||||
this.episodes = this.episodes.filter(ep => ep.id !== episodeId)
|
||||
}
|
||||
}
|
||||
module.exports = Podcast
|
|
@ -52,10 +52,10 @@ class MediaProgress {
|
|||
return !this.isFinished && this.progress > 0
|
||||
}
|
||||
|
||||
setData(libraryItemId, progress) {
|
||||
setData(libraryItemId, progress, episodeId = null) {
|
||||
this.id = libraryItemId
|
||||
this.libraryItemId = libraryItemId
|
||||
this.episodeId = progress.episodeId || null
|
||||
this.episodeId = episodeId
|
||||
this.duration = progress.duration || 0
|
||||
this.progress = Math.min(1, (progress.progress || 0))
|
||||
this.currentTime = progress.currentTime || 0
|
||||
|
@ -74,11 +74,11 @@ class MediaProgress {
|
|||
for (const key in payload) {
|
||||
if (this[key] !== undefined && payload[key] !== this[key]) {
|
||||
if (key === 'isFinished') {
|
||||
if (!payload[key]) { // Updating to Not Read - Reset progress and current time
|
||||
if (!payload[key]) { // Updating to Not Finished - Reset progress and current time
|
||||
this.finishedAt = null
|
||||
this.progress = 0
|
||||
this.currentTime = 0
|
||||
} else { // Updating to Read
|
||||
} else { // Updating to Finished
|
||||
if (!this.finishedAt) this.finishedAt = Date.now()
|
||||
this.progress = 1
|
||||
}
|
||||
|
@ -88,6 +88,16 @@ class MediaProgress {
|
|||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
if (this.progress >= 1 && !this.isFinished) {
|
||||
this.isFinished = true
|
||||
this.finishedAt = Date.now()
|
||||
this.progress = 1
|
||||
} else if (this.progress < 1 && this.isFinished) {
|
||||
this.isFinished = false
|
||||
this.finishedAt = null
|
||||
}
|
||||
|
||||
if (!this.startedAt) {
|
||||
this.startedAt = Date.now()
|
||||
}
|
||||
|
|
|
@ -236,17 +236,23 @@ class User {
|
|||
}
|
||||
}
|
||||
|
||||
getMediaProgress(libraryItemId) {
|
||||
getMediaProgress(libraryItemId, episodeId = null) {
|
||||
if (!this.mediaProgress) return null
|
||||
return this.mediaProgress.find(lip => lip.id === libraryItemId)
|
||||
return this.mediaProgress.find(lip => {
|
||||
if (episodeId && lip.episodeId !== episodeId) return false
|
||||
return lip.id === libraryItemId
|
||||
})
|
||||
}
|
||||
|
||||
createUpdateMediaProgress(libraryItem, updatePayload) {
|
||||
var itemProgress = this.mediaProgress.find(li => li.id === libraryItem.id)
|
||||
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
||||
var itemProgress = this.mediaProgress.find(li => {
|
||||
if (episodeId && li.episodeId !== episodeId) return false
|
||||
return li.id === libraryItem.id
|
||||
})
|
||||
if (!itemProgress) {
|
||||
var newItemProgress = new MediaProgress()
|
||||
|
||||
newItemProgress.setData(libraryItem.id, updatePayload)
|
||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
||||
this.mediaProgress.push(newItemProgress)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -86,7 +86,10 @@ class ApiRouter {
|
|||
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
||||
this.router.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.bind(this))
|
||||
this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
||||
this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
|
||||
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||
this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this))
|
||||
this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this))
|
||||
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
|
@ -126,6 +129,7 @@ class ApiRouter {
|
|||
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
||||
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
||||
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
||||
this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.bind(this))
|
||||
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
||||
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue