mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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