mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:OPML Export #1260
This commit is contained in:
		
							parent
							
								
									019063e6f4
								
							
						
					
					
						commit
						15aaf2863c
					
				| @ -81,6 +81,8 @@ | |||||||
| 
 | 
 | ||||||
|         <!-- issues page remove all button --> |         <!-- issues page remove all button --> | ||||||
|         <ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn> |         <ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn> | ||||||
|  | 
 | ||||||
|  |         <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" menu-width="110px" class="ml-2" @action="contextMenuAction" /> | ||||||
|       </template> |       </template> | ||||||
|       <!-- search page --> |       <!-- search page --> | ||||||
|       <template v-else-if="page === 'search'"> |       <template v-else-if="page === 'search'"> | ||||||
| @ -186,6 +188,9 @@ export default { | |||||||
|     userCanUpdate() { |     userCanUpdate() { | ||||||
|       return this.$store.getters['user/getUserCanUpdate'] |       return this.$store.getters['user/getUserCanUpdate'] | ||||||
|     }, |     }, | ||||||
|  |     userCanDownload() { | ||||||
|  |       return this.$store.getters['user/getUserCanDownload'] | ||||||
|  |     }, | ||||||
|     currentLibraryId() { |     currentLibraryId() { | ||||||
|       return this.$store.state.libraries.currentLibraryId |       return this.$store.state.libraries.currentLibraryId | ||||||
|     }, |     }, | ||||||
| @ -276,9 +281,29 @@ export default { | |||||||
|     }, |     }, | ||||||
|     isIssuesFilter() { |     isIssuesFilter() { | ||||||
|       return this.filterBy === 'issues' && this.$route.query.filter === 'issues' |       return this.filterBy === 'issues' && this.$route.query.filter === 'issues' | ||||||
|  |     }, | ||||||
|  |     contextMenuItems() { | ||||||
|  |       const items = [] | ||||||
|  | 
 | ||||||
|  |       if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) { | ||||||
|  |         items.push({ | ||||||
|  |           text: 'Export OPML', | ||||||
|  |           action: 'export-opml' | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return items | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     contextMenuAction(action) { | ||||||
|  |       if (action === 'export-opml') { | ||||||
|  |         this.exportOPML() | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     exportOPML() { | ||||||
|  |       this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true) | ||||||
|  |     }, | ||||||
|     seriesContextMenuAction(action) { |     seriesContextMenuAction(action) { | ||||||
|       if (action === 'open-rss-feed') { |       if (action === 'open-rss-feed') { | ||||||
|         this.showOpenSeriesRSSFeed() |         this.showOpenSeriesRSSFeed() | ||||||
|  | |||||||
| @ -107,15 +107,7 @@ export default { | |||||||
|       this.$store.commit('globals/setConfirmPrompt', payload) |       this.$store.commit('globals/setConfirmPrompt', payload) | ||||||
|     }, |     }, | ||||||
|     downloadLibraryFile() { |     downloadLibraryFile() { | ||||||
|       const a = document.createElement('a') |       this.$downloadFile(this.downloadUrl, this.track.metadata.filename) | ||||||
|       a.style.display = 'none' |  | ||||||
|       a.href = this.downloadUrl |  | ||||||
|       a.download = this.track.metadata.filename |  | ||||||
|       document.body.appendChild(a) |  | ||||||
|       a.click() |  | ||||||
|       setTimeout(() => { |  | ||||||
|         a.remove() |  | ||||||
|       }) |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() {} |   mounted() {} | ||||||
|  | |||||||
| @ -102,15 +102,7 @@ export default { | |||||||
|       this.$store.commit('globals/setConfirmPrompt', payload) |       this.$store.commit('globals/setConfirmPrompt', payload) | ||||||
|     }, |     }, | ||||||
|     downloadLibraryFile() { |     downloadLibraryFile() { | ||||||
|       const a = document.createElement('a') |       this.$downloadFile(this.downloadUrl, this.file.metadata.filename) | ||||||
|       a.style.display = 'none' |  | ||||||
|       a.href = this.downloadUrl |  | ||||||
|       a.download = this.file.metadata.filename |  | ||||||
|       document.body.appendChild(a) |  | ||||||
|       a.click() |  | ||||||
|       setTimeout(() => { |  | ||||||
|         a.remove() |  | ||||||
|       }) |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() {} |   mounted() {} | ||||||
|  | |||||||
| @ -677,14 +677,7 @@ export default { | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     downloadLibraryItem() { |     downloadLibraryItem() { | ||||||
|       const a = document.createElement('a') |       this.$downloadFile(this.downloadUrl) | ||||||
|       a.style.display = 'none' |  | ||||||
|       a.href = this.downloadUrl |  | ||||||
|       document.body.appendChild(a) |  | ||||||
|       a.click() |  | ||||||
|       setTimeout(() => { |  | ||||||
|         a.remove() |  | ||||||
|       }) |  | ||||||
|     }, |     }, | ||||||
|     deleteLibraryItem() { |     deleteLibraryItem() { | ||||||
|       const payload = { |       const payload = { | ||||||
|  | |||||||
| @ -145,6 +145,25 @@ Vue.prototype.$getNextScheduledDate = (expression) => { | |||||||
|   return interval.next().toDate() |   return interval.next().toDate() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Vue.prototype.$downloadFile = (url, filename = null, openInNewTab = false) => { | ||||||
|  |   const a = document.createElement('a') | ||||||
|  |   a.style.display = 'none' | ||||||
|  |   a.href = url | ||||||
|  | 
 | ||||||
|  |   if (filename) { | ||||||
|  |     a.download = filename | ||||||
|  |   } | ||||||
|  |   if (openInNewTab) { | ||||||
|  |     a.target = '_blank' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   document.body.appendChild(a) | ||||||
|  |   a.click() | ||||||
|  |   setTimeout(() => { | ||||||
|  |     a.remove() | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function supplant(str, subs) { | export function supplant(str, subs) { | ||||||
|   // source: http://crockford.com/javascript/remedial.html
 |   // source: http://crockford.com/javascript/remedial.html
 | ||||||
|   return str.replace(/{([^{}]*)}/g, |   return str.replace(/{([^{}]*)}/g, | ||||||
|  | |||||||
| @ -848,6 +848,12 @@ class LibraryController { | |||||||
|     res.json(payload) |     res.json(payload) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getOPMLFile(req, res) { | ||||||
|  |     const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems) | ||||||
|  |     res.type('application/xml') | ||||||
|  |     res.send(opmlText) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   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}`) | ||||||
|  | |||||||
| @ -109,7 +109,7 @@ class PodcastController { | |||||||
|     res.json({ podcast }) |     res.json({ podcast }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getOPMLFeeds(req, res) { |   async getFeedsFromOPMLText(req, res) { | ||||||
|     if (!req.body.opmlText) { |     if (!req.body.opmlText) { | ||||||
|       return res.sendStatus(400) |       return res.sendStatus(400) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ const { removeFile, downloadFile } = require('../utils/fileUtils') | |||||||
| const filePerms = require('../utils/filePerms') | const filePerms = require('../utils/filePerms') | ||||||
| const { levenshteinDistance } = require('../utils/index') | const { levenshteinDistance } = require('../utils/index') | ||||||
| const opmlParser = require('../utils/parsers/parseOPML') | const opmlParser = require('../utils/parsers/parseOPML') | ||||||
|  | const opmlGenerator = require('../utils/generators/opmlGenerator') | ||||||
| const prober = require('../utils/prober') | const prober = require('../utils/prober') | ||||||
| const ffmpegHelpers = require('../utils/ffmpegHelpers') | const ffmpegHelpers = require('../utils/ffmpegHelpers') | ||||||
| 
 | 
 | ||||||
| @ -373,6 +374,10 @@ class PodcastManager { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   generateOPMLFileText(libraryItems) { | ||||||
|  |     return opmlGenerator.generate(libraryItems) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getDownloadQueueDetails(libraryId = null) { |   getDownloadQueueDetails(libraryId = null) { | ||||||
|     let _currentDownload = this.currentDownload |     let _currentDownload = this.currentDownload | ||||||
|     if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null |     if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ const fs = require('../libs/fsExtra') | |||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const { version } = require('../../package.json') | const { version } = require('../../package.json') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const abmetadataGenerator = require('../utils/abmetadataGenerator') | const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') | ||||||
| const LibraryFile = require('./files/LibraryFile') | const LibraryFile = require('./files/LibraryFile') | ||||||
| const Book = require('./mediaTypes/Book') | const Book = require('./mediaTypes/Book') | ||||||
| const Podcast = require('./mediaTypes/Podcast') | const Podcast = require('./mediaTypes/Podcast') | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ const Ffmpeg = require('../libs/fluentFfmpeg') | |||||||
| const { secondsToTimestamp } = require('../utils/index') | const { secondsToTimestamp } = require('../utils/index') | ||||||
| const { writeConcatFile } = require('../utils/ffmpegHelpers') | const { writeConcatFile } = require('../utils/ffmpegHelpers') | ||||||
| const { AudioMimeType } = require('../utils/constants') | const { AudioMimeType } = require('../utils/constants') | ||||||
| const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator') | const hlsPlaylistGenerator = require('../utils/generators/hlsPlaylistGenerator') | ||||||
| const AudioTrack = require('./files/AudioTrack') | const AudioTrack = require('./files/AudioTrack') | ||||||
| 
 | 
 | ||||||
| class Stream extends EventEmitter { | class Stream extends EventEmitter { | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ const BookMetadata = require('../metadata/BookMetadata') | |||||||
| const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') | const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') | ||||||
| const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata') | const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata') | ||||||
| const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers') | const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers') | ||||||
| const abmetadataGenerator = require('../../utils/abmetadataGenerator') | const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator') | ||||||
| const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') | const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') | ||||||
| const AudioFile = require('../files/AudioFile') | const AudioFile = require('../files/AudioFile') | ||||||
| const AudioTrack = require('../files/AudioTrack') | const AudioTrack = require('../files/AudioTrack') | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ const Logger = require('../../Logger') | |||||||
| const PodcastEpisode = require('../entities/PodcastEpisode') | const PodcastEpisode = require('../entities/PodcastEpisode') | ||||||
| const PodcastMetadata = require('../metadata/PodcastMetadata') | const PodcastMetadata = require('../metadata/PodcastMetadata') | ||||||
| const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') | const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') | ||||||
| const abmetadataGenerator = require('../../utils/abmetadataGenerator') | const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator') | ||||||
| const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') | const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') | ||||||
| const { createNewSortInstance } = require('../../libs/fastSort') | const { createNewSortInstance } = require('../../libs/fastSort') | ||||||
| const naturalSort = createNewSortInstance({ | const naturalSort = createNewSortInstance({ | ||||||
|  | |||||||
| @ -90,7 +90,7 @@ class ApiRouter { | |||||||
|     this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this)) |     this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this)) | ||||||
|     this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this)) |     this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this)) | ||||||
|     this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) |     this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this)) | ||||||
| 
 |     this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this)) | ||||||
|     this.router.post('/libraries/order', LibraryController.reorder.bind(this)) |     this.router.post('/libraries/order', LibraryController.reorder.bind(this)) | ||||||
| 
 | 
 | ||||||
|     //
 |     //
 | ||||||
| @ -236,7 +236,7 @@ class ApiRouter { | |||||||
|     //
 |     //
 | ||||||
|     this.router.post('/podcasts', PodcastController.create.bind(this)) |     this.router.post('/podcasts', PodcastController.create.bind(this)) | ||||||
|     this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this)) |     this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this)) | ||||||
|     this.router.post('/podcasts/opml', PodcastController.getOPMLFeeds.bind(this)) |     this.router.post('/podcasts/opml', PodcastController.getFeedsFromOPMLText.bind(this)) | ||||||
|     this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this)) |     this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this)) | ||||||
|     this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this)) |     this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this)) | ||||||
|     this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this)) |     this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this)) | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| const fs = require('../libs/fsExtra') | const fs = require('../../libs/fsExtra') | ||||||
| const filePerms = require('./filePerms') | const filePerms = require('../filePerms') | ||||||
| const package = require('../../package.json') | const package = require('../../../package.json') | ||||||
| const Logger = require('../Logger') | const Logger = require('../../Logger') | ||||||
| const { getId } = require('./index') | const { getId } = require('../index') | ||||||
| const areEquivalent = require('../utils/areEquivalent') | const areEquivalent = require('../areEquivalent') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const CurrentAbMetadataVersion = 2 | const CurrentAbMetadataVersion = 2 | ||||||
| @ -1,4 +1,4 @@ | |||||||
| const fs = require('../libs/fsExtra') | const fs = require('../../libs/fsExtra') | ||||||
| 
 | 
 | ||||||
| function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, token) { | function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, token) { | ||||||
|   var ext = hlsSegmentType === 'fmp4' ? 'm4s' : 'ts' |   var ext = hlsSegmentType === 'fmp4' ? 'm4s' : 'ts' | ||||||
							
								
								
									
										52
									
								
								server/utils/generators/opmlGenerator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								server/utils/generators/opmlGenerator.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | const xml = require('../../libs/xml') | ||||||
|  | 
 | ||||||
|  | module.exports.generate = (libraryItems, indent = true) => { | ||||||
|  |   const bodyItems = [] | ||||||
|  |   libraryItems.forEach((item) => { | ||||||
|  |     if (!item.media.metadata.feedUrl) return | ||||||
|  |     const feedAttributes = { | ||||||
|  |       type: 'rss', | ||||||
|  |       text: item.media.metadata.title, | ||||||
|  |       title: item.media.metadata.title, | ||||||
|  |       xmlUrl: item.media.metadata.feedUrl | ||||||
|  |     } | ||||||
|  |     if (item.media.metadata.description) { | ||||||
|  |       feedAttributes.description = item.media.metadata.description | ||||||
|  |     } | ||||||
|  |     if (item.media.metadata.itunesPageUrl) { | ||||||
|  |       feedAttributes.htmlUrl = item.media.metadata.itunesPageUrl | ||||||
|  |     } | ||||||
|  |     if (item.media.metadata.language) { | ||||||
|  |       feedAttributes.language = item.media.metadata.language | ||||||
|  |     } | ||||||
|  |     bodyItems.push({ | ||||||
|  |       outline: { | ||||||
|  |         _attr: feedAttributes | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const data = [ | ||||||
|  |     { | ||||||
|  |       opml: [ | ||||||
|  |         { | ||||||
|  |           _attr: { | ||||||
|  |             version: '1.0' | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           head: [ | ||||||
|  |             { | ||||||
|  |               title: 'Audiobookshelf Podcast Subscriptions' | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           body: bodyItems | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | 
 | ||||||
|  |   return '<?xml version="1.0" encoding="UTF-8"?>\n' + xml(data, indent) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user