mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Merge master
This commit is contained in:
commit
1d13d0a553
@ -303,13 +303,13 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch update success!')
|
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch update failed')
|
this.$toast.error(this.$strings.ToastBatchUpdateFailed)
|
||||||
console.error('Failed to batch update read/not read', error)
|
console.error('Failed to batch update read/not read', error)
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
})
|
})
|
||||||
|
@ -680,7 +680,6 @@ export default {
|
|||||||
.$patch(apiEndpoint, updatePayload)
|
.$patch(apiEndpoint, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -110,7 +110,10 @@ export default {
|
|||||||
itemEpisodeMap() {
|
itemEpisodeMap() {
|
||||||
const map = {}
|
const map = {}
|
||||||
this.itemEpisodes.forEach((item) => {
|
this.itemEpisodes.forEach((item) => {
|
||||||
if (item.enclosure) map[item.enclosure.url.split('?')[0]] = true
|
if (item.enclosure) {
|
||||||
|
const cleanUrl = this.getCleanEpisodeUrl(item.enclosure.url)
|
||||||
|
map[cleanUrl] = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
@ -129,6 +132,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* RSS feed episode url is used for matching with existing downloaded episodes.
|
||||||
|
* Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests.
|
||||||
|
* These need to be removed in order to detect the same episode each time the feed is pulled.
|
||||||
|
*
|
||||||
|
* An RSS feed may include an `id` in the query string. In these cases we want to leave the `id`.
|
||||||
|
* @see https://github.com/advplyr/audiobookshelf/issues/1896
|
||||||
|
*
|
||||||
|
* @param {string} url - rss feed episode url
|
||||||
|
* @returns {string} rss feed episode url without dynamic query strings
|
||||||
|
*/
|
||||||
|
getCleanEpisodeUrl(url) {
|
||||||
|
let queryString = url.split('?')[1]
|
||||||
|
if (!queryString) return url
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(queryString)
|
||||||
|
for (const p of Array.from(searchParams.keys())) {
|
||||||
|
if (p !== 'id') searchParams.delete(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchParams.toString()) return url
|
||||||
|
return `${url}?${searchParams.toString()}`
|
||||||
|
},
|
||||||
inputUpdate() {
|
inputUpdate() {
|
||||||
clearTimeout(this.searchTimeout)
|
clearTimeout(this.searchTimeout)
|
||||||
this.searchTimeout = setTimeout(() => {
|
this.searchTimeout = setTimeout(() => {
|
||||||
@ -198,7 +224,7 @@ export default {
|
|||||||
.map((_ep) => {
|
.map((_ep) => {
|
||||||
return {
|
return {
|
||||||
..._ep,
|
..._ep,
|
||||||
cleanUrl: _ep.enclosure.url.split('?')[0]
|
cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
||||||
|
@ -188,7 +188,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.book.id}`, updatePayload)
|
.$patch(`/api/me/progress/${this.book.id}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -198,7 +198,6 @@ export default {
|
|||||||
.$patch(routepath, updatePayload)
|
.$patch(routepath, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -183,7 +183,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -533,7 +533,6 @@ export default {
|
|||||||
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -19,7 +19,7 @@ export default {
|
|||||||
return redirect(`/library/${libraryId}`)
|
return redirect(`/library/${libraryId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const series = await app.$axios.$get(`/api/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
const series = await app.$axios.$get(`/api/libraries/${library.id}/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -320,7 +320,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 4000 }).catch((error) => {
|
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 6000 }).catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -340,12 +340,13 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 6000 }).then(() => {
|
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 9000 }).then(() => {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Failed to update session progress', error)
|
console.error('Failed to update session progress', error)
|
||||||
|
// After 4 failed sync attempts show an alert toast
|
||||||
this.failedProgressSyncs++
|
this.failedProgressSyncs++
|
||||||
if (this.failedProgressSyncs >= 2) {
|
if (this.failedProgressSyncs >= 4) {
|
||||||
this.ctx.showFailedProgressSyncs()
|
this.ctx.showFailedProgressSyncs()
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
}
|
}
|
||||||
|
@ -391,7 +391,13 @@ class LibraryController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/series
|
/**
|
||||||
|
* api/libraries/:id/series
|
||||||
|
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
|
||||||
|
*
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getAllSeriesForLibrary(req, res) {
|
async getAllSeriesForLibrary(req, res) {
|
||||||
const libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
|
|
||||||
@ -452,6 +458,42 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api/libraries/:id/series/:seriesId
|
||||||
|
*
|
||||||
|
* Optional includes (e.g. `?include=rssfeed,progress`)
|
||||||
|
* rssfeed: adds `rssFeed` to series object if a feed is open
|
||||||
|
* progress: adds `progress` to series object with { libraryItemIds:Array<llid>, libraryItemIdsFinished:Array<llid>, isFinished:boolean }
|
||||||
|
*
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res - Series
|
||||||
|
*/
|
||||||
|
async getSeriesForLibrary(req, res) {
|
||||||
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
|
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||||
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
|
const libraryItemsInSeries = req.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||||
|
|
||||||
|
const seriesJson = series.toJSON()
|
||||||
|
if (include.includes('progress')) {
|
||||||
|
const libraryItemsFinished = libraryItemsInSeries.filter(li => !!req.user.getMediaProgress(li.id)?.isFinished)
|
||||||
|
seriesJson.progress = {
|
||||||
|
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
||||||
|
libraryItemIdsFinished: libraryItemsFinished.map(li => li.id),
|
||||||
|
isFinished: libraryItemsFinished.length >= libraryItemsInSeries.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include.includes('rssfeed')) {
|
||||||
|
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||||
|
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(seriesJson)
|
||||||
|
}
|
||||||
|
|
||||||
// api/libraries/:id/collections
|
// api/libraries/:id/collections
|
||||||
async getCollectionsForLibrary(req, res) {
|
async getCollectionsForLibrary(req, res) {
|
||||||
const libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
@ -859,7 +901,7 @@ class LibraryController {
|
|||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
||||||
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const library = Database.libraries.find(lib => lib.id === req.params.id)
|
const library = Database.libraries.find(lib => lib.id === req.params.id)
|
||||||
|
@ -5,6 +5,16 @@ const Database = require('../Database')
|
|||||||
class SeriesController {
|
class SeriesController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* /api/series/:id
|
||||||
|
*
|
||||||
|
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
|
||||||
|
* Series are not library specific so we need to know what the library id is
|
||||||
|
*
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v)
|
const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v)
|
||||||
|
|
||||||
@ -29,7 +39,7 @@ class SeriesController {
|
|||||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(seriesJson)
|
res.json(seriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(req, res) {
|
async search(req, res) {
|
||||||
@ -56,9 +66,13 @@ class SeriesController {
|
|||||||
const series = Database.series.find(se => se.id === req.params.id)
|
const series = Database.series.find(se => se.id === req.params.id)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
const libraryItemsInSeries = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
/**
|
||||||
if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) {
|
* Filter out any library items not accessible to user
|
||||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user)
|
*/
|
||||||
|
const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||||
|
const libraryItemsAccessible = libraryItems.filter(req.user.checkCanAccessLibraryItem)
|
||||||
|
if (libraryItems.length && !libraryItemsAccessible.length) {
|
||||||
|
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +85,7 @@ class SeriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.series = series
|
req.series = series
|
||||||
req.libraryItemsInSeries = libraryItemsInSeries
|
req.libraryItemsInSeries = libraryItemsAccessible
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@ class ApiRouter {
|
|||||||
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
||||||
this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
|
this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
|
||||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
|
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
|
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this))
|
this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this))
|
||||||
|
Loading…
Reference in New Issue
Block a user