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 --> | ||||
|         <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> | ||||
|       <!-- search page --> | ||||
|       <template v-else-if="page === 'search'"> | ||||
| @ -186,6 +188,9 @@ export default { | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     userCanDownload() { | ||||
|       return this.$store.getters['user/getUserCanDownload'] | ||||
|     }, | ||||
|     currentLibraryId() { | ||||
|       return this.$store.state.libraries.currentLibraryId | ||||
|     }, | ||||
| @ -276,9 +281,29 @@ export default { | ||||
|     }, | ||||
|     isIssuesFilter() { | ||||
|       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: { | ||||
|     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) { | ||||
|       if (action === 'open-rss-feed') { | ||||
|         this.showOpenSeriesRSSFeed() | ||||
|  | ||||
| @ -107,15 +107,7 @@ export default { | ||||
|       this.$store.commit('globals/setConfirmPrompt', payload) | ||||
|     }, | ||||
|     downloadLibraryFile() { | ||||
|       const a = document.createElement('a') | ||||
|       a.style.display = 'none' | ||||
|       a.href = this.downloadUrl | ||||
|       a.download = this.track.metadata.filename | ||||
|       document.body.appendChild(a) | ||||
|       a.click() | ||||
|       setTimeout(() => { | ||||
|         a.remove() | ||||
|       }) | ||||
|       this.$downloadFile(this.downloadUrl, this.track.metadata.filename) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -102,15 +102,7 @@ export default { | ||||
|       this.$store.commit('globals/setConfirmPrompt', payload) | ||||
|     }, | ||||
|     downloadLibraryFile() { | ||||
|       const a = document.createElement('a') | ||||
|       a.style.display = 'none' | ||||
|       a.href = this.downloadUrl | ||||
|       a.download = this.file.metadata.filename | ||||
|       document.body.appendChild(a) | ||||
|       a.click() | ||||
|       setTimeout(() => { | ||||
|         a.remove() | ||||
|       }) | ||||
|       this.$downloadFile(this.downloadUrl, this.file.metadata.filename) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -677,14 +677,7 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     downloadLibraryItem() { | ||||
|       const a = document.createElement('a') | ||||
|       a.style.display = 'none' | ||||
|       a.href = this.downloadUrl | ||||
|       document.body.appendChild(a) | ||||
|       a.click() | ||||
|       setTimeout(() => { | ||||
|         a.remove() | ||||
|       }) | ||||
|       this.$downloadFile(this.downloadUrl) | ||||
|     }, | ||||
|     deleteLibraryItem() { | ||||
|       const payload = { | ||||
|  | ||||
| @ -145,6 +145,25 @@ Vue.prototype.$getNextScheduledDate = (expression) => { | ||||
|   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) { | ||||
|   // source: http://crockford.com/javascript/remedial.html
 | ||||
|   return str.replace(/{([^{}]*)}/g, | ||||
|  | ||||
| @ -848,6 +848,12 @@ class LibraryController { | ||||
|     res.json(payload) | ||||
|   } | ||||
| 
 | ||||
|   getOPMLFile(req, res) { | ||||
|     const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems) | ||||
|     res.type('application/xml') | ||||
|     res.send(opmlText) | ||||
|   } | ||||
| 
 | ||||
|   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}`) | ||||
|  | ||||
| @ -109,7 +109,7 @@ class PodcastController { | ||||
|     res.json({ podcast }) | ||||
|   } | ||||
| 
 | ||||
|   async getOPMLFeeds(req, res) { | ||||
|   async getFeedsFromOPMLText(req, res) { | ||||
|     if (!req.body.opmlText) { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
|  | ||||
| @ -8,6 +8,7 @@ const { removeFile, downloadFile } = require('../utils/fileUtils') | ||||
| const filePerms = require('../utils/filePerms') | ||||
| const { levenshteinDistance } = require('../utils/index') | ||||
| const opmlParser = require('../utils/parsers/parseOPML') | ||||
| const opmlGenerator = require('../utils/generators/opmlGenerator') | ||||
| const prober = require('../utils/prober') | ||||
| const ffmpegHelpers = require('../utils/ffmpegHelpers') | ||||
| 
 | ||||
| @ -373,6 +374,10 @@ class PodcastManager { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   generateOPMLFileText(libraryItems) { | ||||
|     return opmlGenerator.generate(libraryItems) | ||||
|   } | ||||
| 
 | ||||
|   getDownloadQueueDetails(libraryId = null) { | ||||
|     let _currentDownload = this.currentDownload | ||||
|     if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null | ||||
|  | ||||
| @ -2,7 +2,7 @@ const fs = require('../libs/fsExtra') | ||||
| const Path = require('path') | ||||
| const { version } = require('../../package.json') | ||||
| const Logger = require('../Logger') | ||||
| const abmetadataGenerator = require('../utils/abmetadataGenerator') | ||||
| const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') | ||||
| const LibraryFile = require('./files/LibraryFile') | ||||
| const Book = require('./mediaTypes/Book') | ||||
| const Podcast = require('./mediaTypes/Podcast') | ||||
|  | ||||
| @ -10,7 +10,7 @@ const Ffmpeg = require('../libs/fluentFfmpeg') | ||||
| const { secondsToTimestamp } = require('../utils/index') | ||||
| const { writeConcatFile } = require('../utils/ffmpegHelpers') | ||||
| const { AudioMimeType } = require('../utils/constants') | ||||
| const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator') | ||||
| const hlsPlaylistGenerator = require('../utils/generators/hlsPlaylistGenerator') | ||||
| const AudioTrack = require('./files/AudioTrack') | ||||
| 
 | ||||
| class Stream extends EventEmitter { | ||||
|  | ||||
| @ -4,7 +4,7 @@ const BookMetadata = require('../metadata/BookMetadata') | ||||
| const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index') | ||||
| const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata') | ||||
| const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers') | ||||
| const abmetadataGenerator = require('../../utils/abmetadataGenerator') | ||||
| const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator') | ||||
| const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils') | ||||
| const AudioFile = require('../files/AudioFile') | ||||
| const AudioTrack = require('../files/AudioTrack') | ||||
|  | ||||
| @ -2,7 +2,7 @@ const Logger = require('../../Logger') | ||||
| const PodcastEpisode = require('../entities/PodcastEpisode') | ||||
| const PodcastMetadata = require('../metadata/PodcastMetadata') | ||||
| 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 { createNewSortInstance } = require('../../libs/fastSort') | ||||
| 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.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/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.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/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/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.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 filePerms = require('./filePerms') | ||||
| const package = require('../../package.json') | ||||
| const Logger = require('../Logger') | ||||
| const { getId } = require('./index') | ||||
| const areEquivalent = require('../utils/areEquivalent') | ||||
| const fs = require('../../libs/fsExtra') | ||||
| const filePerms = require('../filePerms') | ||||
| const package = require('../../../package.json') | ||||
| const Logger = require('../../Logger') | ||||
| const { getId } = require('../index') | ||||
| const areEquivalent = require('../areEquivalent') | ||||
| 
 | ||||
| 
 | ||||
| const CurrentAbMetadataVersion = 2 | ||||
| @ -1,4 +1,4 @@ | ||||
| const fs = require('../libs/fsExtra') | ||||
| const fs = require('../../libs/fsExtra') | ||||
| 
 | ||||
| function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, token) { | ||||
|   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