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 | ||||
|         .patch(`/api/me/progress/batch/update`, updateProgressPayloads) | ||||
|         .then(() => { | ||||
|           this.$toast.success('Batch update success!') | ||||
|           this.$toast.success(this.$strings.ToastBatchUpdateSuccess) | ||||
|           this.$store.commit('setProcessingBatch', false) | ||||
|           this.$store.commit('globals/resetSelectedMediaItems', []) | ||||
|           this.$eventBus.$emit('bookshelf_clear_selection') | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           this.$toast.error('Batch update failed') | ||||
|           this.$toast.error(this.$strings.ToastBatchUpdateFailed) | ||||
|           console.error('Failed to batch update read/not read', error) | ||||
|           this.$store.commit('setProcessingBatch', false) | ||||
|         }) | ||||
|  | ||||
| @ -680,7 +680,6 @@ export default { | ||||
|         .$patch(apiEndpoint, updatePayload) | ||||
|         .then(() => { | ||||
|           this.processing = false | ||||
|           toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|  | ||||
| @ -110,7 +110,10 @@ export default { | ||||
|     itemEpisodeMap() { | ||||
|       const map = {} | ||||
|       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 | ||||
|     }, | ||||
| @ -129,6 +132,29 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   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() { | ||||
|       clearTimeout(this.searchTimeout) | ||||
|       this.searchTimeout = setTimeout(() => { | ||||
| @ -198,7 +224,7 @@ export default { | ||||
|         .map((_ep) => { | ||||
|           return { | ||||
|             ..._ep, | ||||
|             cleanUrl: _ep.enclosure.url.split('?')[0] | ||||
|             cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url) | ||||
|           } | ||||
|         }) | ||||
|       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) | ||||
|         .then(() => { | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|  | ||||
| @ -198,7 +198,6 @@ export default { | ||||
|         .$patch(routepath, updatePayload) | ||||
|         .then(() => { | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|  | ||||
| @ -183,7 +183,6 @@ export default { | ||||
|         .$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload) | ||||
|         .then(() => { | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|  | ||||
| @ -533,7 +533,6 @@ export default { | ||||
|         .$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload) | ||||
|         .then(() => { | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|  | ||||
| @ -19,7 +19,7 @@ export default { | ||||
|       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) | ||||
|       return false | ||||
|     }) | ||||
|  | ||||
| @ -320,7 +320,7 @@ export default class PlayerHandler { | ||||
|     } | ||||
|     this.listeningTimeSinceSync = 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) | ||||
|     }) | ||||
|   } | ||||
| @ -340,12 +340,13 @@ export default class PlayerHandler { | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|     }).catch((error) => { | ||||
|       console.error('Failed to update session progress', error) | ||||
|       // After 4 failed sync attempts show an alert toast
 | ||||
|       this.failedProgressSyncs++ | ||||
|       if (this.failedProgressSyncs >= 2) { | ||||
|       if (this.failedProgressSyncs >= 4) { | ||||
|         this.ctx.showFailedProgressSyncs() | ||||
|         this.failedProgressSyncs = 0 | ||||
|       } | ||||
|  | ||||
| @ -391,7 +391,13 @@ class LibraryController { | ||||
|     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) { | ||||
|     const libraryItems = req.libraryItems | ||||
| 
 | ||||
| @ -452,6 +458,42 @@ class LibraryController { | ||||
|     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
 | ||||
|   async getCollectionsForLibrary(req, res) { | ||||
|     const libraryItems = req.libraryItems | ||||
| @ -859,7 +901,7 @@ class LibraryController { | ||||
|   middleware(req, res, next) { | ||||
|     if (!req.user.checkCanAccessLibrary(req.params.id)) { | ||||
|       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) | ||||
|  | ||||
| @ -5,6 +5,16 @@ const Database = require('../Database') | ||||
| class SeriesController { | ||||
|   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) { | ||||
|     const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v) | ||||
| 
 | ||||
| @ -29,7 +39,7 @@ class SeriesController { | ||||
|       seriesJson.rssFeed = feedObj?.toJSONMinified() || null | ||||
|     } | ||||
| 
 | ||||
|     return res.json(seriesJson) | ||||
|     res.json(seriesJson) | ||||
|   } | ||||
| 
 | ||||
|   async search(req, res) { | ||||
| @ -56,9 +66,13 @@ class SeriesController { | ||||
|     const series = Database.series.find(se => se.id === req.params.id) | ||||
|     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))) { | ||||
|       Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user) | ||||
|     /** | ||||
|      * Filter out any library items not accessible to 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) | ||||
|     } | ||||
| 
 | ||||
| @ -71,7 +85,7 @@ class SeriesController { | ||||
|     } | ||||
| 
 | ||||
|     req.series = series | ||||
|     req.libraryItemsInSeries = libraryItemsInSeries | ||||
|     req.libraryItemsInSeries = libraryItemsAccessible | ||||
|     next() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -78,6 +78,7 @@ class ApiRouter { | ||||
|     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/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/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) | ||||
|     this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this)) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user