mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +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