diff --git a/client/components/modals/rssfeed/ViewModal.vue b/client/components/modals/rssfeed/ViewModal.vue
index c4a8c4ab..aa018e82 100644
--- a/client/components/modals/rssfeed/ViewModal.vue
+++ b/client/components/modals/rssfeed/ViewModal.vue
@@ -27,7 +27,6 @@
Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.
-
Note: RSS feed URLs are not authenticated
Close RSS Feed
Open RSS Feed
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index bc80a0f7..8128b798 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -534,13 +534,13 @@ export default {
}
},
rssFeedOpen(data) {
- if (data.libraryItemId === this.libraryItemId) {
+ if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Opened', data)
this.rssFeedUrl = data.feedUrl
}
},
rssFeedClosed(data) {
- if (data.libraryItemId === this.libraryItemId) {
+ if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Closed', data)
this.rssFeedUrl = null
}
diff --git a/server/Db.js b/server/Db.js
index 1a2937af..37e54eb0 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -11,6 +11,7 @@ const Author = require('./objects/entities/Author')
const Series = require('./objects/entities/Series')
const ServerSettings = require('./objects/settings/ServerSettings')
const PlaybackSession = require('./objects/PlaybackSession')
+const Feed = require('./objects/Feed')
class Db {
constructor() {
@@ -22,6 +23,7 @@ class Db {
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
this.SeriesPath = Path.join(global.ConfigPath, 'series')
+ this.FeedsPath = Path.join(global.ConfigPath, 'feeds')
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath)
this.usersDb = new njodb.Database(this.UsersPath)
@@ -31,6 +33,7 @@ class Db {
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
this.authorsDb = new njodb.Database(this.AuthorsPath)
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
+ this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
this.libraryItems = []
this.users = []
@@ -59,6 +62,7 @@ class Db {
else if (entityName === 'collection') return this.collectionsDb
else if (entityName === 'author') return this.authorsDb
else if (entityName === 'series') return this.seriesDb
+ else if (entityName === 'feed') return this.feedsDb
return null
}
@@ -71,6 +75,7 @@ class Db {
else if (entityName === 'collection') return 'collections'
else if (entityName === 'author') return 'authors'
else if (entityName === 'series') return 'series'
+ else if (entityName === 'feed') return 'feeds'
return null
}
@@ -83,6 +88,7 @@ class Db {
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
this.authorsDb = new njodb.Database(this.AuthorsPath)
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
+ this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
return this.init()
}
@@ -116,21 +122,6 @@ class Db {
async init() {
await this.load()
- // Insert Defaults
- // var rootUser = this.users.find(u => u.type === 'root')
- // if (!rootUser) {
- // var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
- // Logger.debug('Generated default token', token)
- // Logger.info('[Db] Root user created')
- // await this.insertEntity('user', this.getDefaultUser(token))
- // } else {
- // Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
- // }
-
- // if (!this.libraries.length) {
- // await this.insertEntity('library', this.getDefaultLibrary())
- // }
-
if (!this.serverSettings) {
this.serverSettings = new ServerSettings()
await this.insertEntity('settings', this.serverSettings)
@@ -278,6 +269,14 @@ class Db {
return this.updateEntity('settings', this.serverSettings)
}
+ getAllEntities(entityName) {
+ var entityDb = this.getEntityDb(entityName)
+ return entityDb.select(() => true).then((results) => results.data).catch((error) => {
+ Logger.error(`[DB] Failed to get all ${entityName}`, error)
+ return null
+ })
+ }
+
insertEntities(entityName, entities) {
var entityDb = this.getEntityDb(entityName)
return entityDb.insert(entities).then((results) => {
diff --git a/server/Server.js b/server/Server.js
index 579d6a79..1c437e25 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -142,6 +142,7 @@ class Server {
await this.backupManager.init()
await this.logManager.init()
+ await this.rssFeedManager.init()
this.podcastManager.init()
if (this.db.serverSettings.scannerDisableWatcher) {
@@ -194,14 +195,14 @@ class Server {
// RSS Feed temp route
app.get('/feed/:id', (req, res) => {
- Logger.info(`[Server] requesting rss feed ${req.params.id}`)
+ Logger.info(`[Server] Requesting rss feed ${req.params.id}`)
this.rssFeedManager.getFeed(req, res)
})
app.get('/feed/:id/cover', (req, res) => {
this.rssFeedManager.getFeedCover(req, res)
})
- app.get('/feed/:id/item/*', (req, res) => {
- Logger.info(`[Server] requesting rss feed ${req.params.id}`)
+ app.get('/feed/:id/item/:episodeId/*', (req, res) => {
+ Logger.debug(`[Server] Requesting rss feed episode ${req.params.id}/${req.params.episodeId}`)
this.rssFeedManager.getFeedItem(req, res)
})
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 416462b3..a40fba09 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -378,7 +378,7 @@ class LibraryItemController {
return res.sendStatus(500)
}
- const feedData = this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
+ const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
if (feedData.error) {
return res.json({
success: false,
@@ -398,7 +398,7 @@ class LibraryItemController {
return res.sendStatus(500)
}
- this.rssFeedManager.closeFeedForItem(req.params.id)
+ await this.rssFeedManager.closeFeedForItem(req.params.id)
res.sendStatus(200)
}
diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js
index 7d46381e..e6ed676b 100644
--- a/server/managers/RssFeedManager.js
+++ b/server/managers/RssFeedManager.js
@@ -2,6 +2,7 @@ const Path = require('path')
const fs = require('fs-extra')
const date = require('date-and-time')
const { Podcast } = require('podcast')
+const Feed = require('../objects/Feed')
const Logger = require('../Logger')
// Not functional at the moment
@@ -12,137 +13,73 @@ class RssFeedManager {
this.feeds = {}
}
+ async init() {
+ var feedObjects = await this.db.getAllEntities('feed')
+ if (feedObjects && feedObjects.length) {
+ feedObjects.forEach((feedObj) => {
+ var feed = new Feed(feedObj)
+ this.feeds[feed.id] = feed
+ Logger.info(`[RssFeedManager] Opened rss feed ${feed.feedUrl}`)
+ })
+ }
+ }
+
findFeedForItem(libraryItemId) {
- return Object.values(this.feeds).find(feed => feed.libraryItemId === libraryItemId)
+ return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId)
}
getFeed(req, res) {
- var feedData = this.feeds[req.params.id]
- if (!feedData) {
+ var feed = this.feeds[req.params.id]
+ if (!feed) {
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
res.sendStatus(404)
return
}
- var xml = feedData.feed.buildXml()
+ // var xml = feedData.feed.buildXml()
+ var xml = feed.buildXml()
res.set('Content-Type', 'text/xml')
res.send(xml)
}
getFeedItem(req, res) {
- var feedData = this.feeds[req.params.id]
- if (!feedData) {
+ var feed = this.feeds[req.params.id]
+ if (!feed) {
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
res.sendStatus(404)
return
}
- var remainingPath = req.params['0']
- var fullPath = Path.join(feedData.libraryItemPath, remainingPath)
- res.sendFile(fullPath)
+ var episodePath = feed.getEpisodePath(req.params.episodeId)
+ if (!episodePath) {
+ Logger.error(`[RssFeedManager] Feed episode not found ${req.params.episodeId}`)
+ res.sendStatus(404)
+ return
+ }
+ res.sendFile(episodePath)
+ // var remainingPath = req.params['0']
+ // var fullPath = Path.join(feedData.libraryItemPath, remainingPath)
+ // res.sendFile(fullPath)
}
getFeedCover(req, res) {
- var feedData = this.feeds[req.params.id]
- if (!feedData) {
+ var feed = this.feeds[req.params.id]
+ if (!feed) {
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
res.sendStatus(404)
return
}
- if (!feedData.mediaCoverPath) {
+ if (!feed.coverPath) {
res.sendStatus(404)
return
}
- const extname = Path.extname(feedData.mediaCoverPath).toLowerCase().slice(1)
+ const extname = Path.extname(feedData.coverPath).toLowerCase().slice(1)
res.type(`image/${extname}`)
- var readStream = fs.createReadStream(feedData.mediaCoverPath)
+ var readStream = fs.createReadStream(feedData.coverPath)
readStream.pipe(res)
}
- openFeed(userId, slug, libraryItem, serverAddress) {
- const media = libraryItem.media
- const mediaMetadata = media.metadata
- const isPodcast = libraryItem.mediaType === 'podcast'
-
- const feedUrl = `${serverAddress}/feed/${slug}`
- const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
-
- const feed = new Podcast({
- title: mediaMetadata.title,
- description: mediaMetadata.description,
- feedUrl,
- siteUrl: `${serverAddress}/items/${libraryItem.id}`,
- imageUrl: media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`,
- author: author || 'advplyr',
- language: 'en'
- })
-
- if (isPodcast) { // PODCAST EPISODES
- media.episodes.forEach((episode) => {
- var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
- contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
-
- feed.addItem({
- title: episode.title,
- description: episode.description || '',
- enclosure: {
- url: `${serverAddress}${contentUrl}`,
- type: episode.audioTrack.mimeType,
- size: episode.size
- },
- date: episode.pubDate || '',
- url: `${serverAddress}${contentUrl}`,
- author: author || 'advplyr'
- })
- })
- } else { // AUDIOBOOK EPISODES
-
- // Example:
Fri, 04 Feb 2015 00:00:00 GMT
- const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
-
- media.tracks.forEach((audioTrack) => {
- var contentUrl = audioTrack.contentUrl.replace(/\\/g, '/')
- contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
-
- var title = audioTrack.title
- if (media.chapters.length) {
- // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
- var matchingChapter = media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
- if (matchingChapter && matchingChapter.title) title = matchingChapter.title
- }
-
- feed.addItem({
- title,
- description: '',
- enclosure: {
- url: `${serverAddress}${contentUrl}`,
- type: audioTrack.mimeType,
- size: audioTrack.metadata.size
- },
- date: audiobookPubDate,
- url: `${serverAddress}${contentUrl}`,
- author: author || 'advplyr'
- })
- })
- }
-
-
- const feedData = {
- id: slug,
- slug,
- userId,
- libraryItemId: libraryItem.id,
- libraryItemPath: libraryItem.path,
- mediaCoverPath: media.coverPath,
- serverAddress: serverAddress,
- feedUrl,
- feed
- }
- this.feeds[slug] = feedData
- return feedData
- }
-
- openFeedForItem(user, libraryItem, options) {
+ async openFeedForItem(user, libraryItem, options) {
const serverAddress = options.serverAddress
const slug = options.slug
@@ -153,24 +90,29 @@ class RssFeedManager {
}
}
- const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress)
- Logger.debug(`[RssFeedManager] Opened RSS feed ${feedData.feedUrl}`)
- this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
- return feedData
+ const feed = new Feed()
+ feed.setFromItem(user.id, slug, libraryItem, serverAddress)
+ this.feeds[feed.id] = feed
+
+ Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`)
+ await this.db.insertEntity('feed', feed)
+ this.emitter('rss_feed_open', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
+ return feed
}
closeFeedForItem(libraryItemId) {
var feed = this.findFeedForItem(libraryItemId)
if (!feed) return
- this.closeRssFeed(feed.id)
+ return this.closeRssFeed(feed.id)
}
- closeRssFeed(id) {
+ async closeRssFeed(id) {
if (!this.feeds[id]) return
- var feedData = this.feeds[id]
- this.emitter('rss_feed_closed', { libraryItemId: feedData.libraryItemId, feedUrl: feedData.feedUrl })
+ var feed = this.feeds[id]
+ await this.db.removeEntity('feed', id)
+ this.emitter('rss_feed_closed', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
delete this.feeds[id]
- Logger.info(`[RssFeedManager] Closed RSS feed "${feedData.feedUrl}"`)
+ Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
}
}
module.exports = RssFeedManager
\ No newline at end of file
diff --git a/server/objects/Feed.js b/server/objects/Feed.js
new file mode 100644
index 00000000..49dc0768
--- /dev/null
+++ b/server/objects/Feed.js
@@ -0,0 +1,118 @@
+const FeedMeta = require('./FeedMeta')
+const FeedEpisode = require('./FeedEpisode')
+const { Podcast } = require('podcast')
+
+class Feed {
+ constructor(feed) {
+ this.id = null
+ this.slug = null
+ this.userId = null
+ this.entityType = null
+ this.entityId = null
+
+ this.coverPath = null
+ this.serverAddress = null
+ this.feedUrl = null
+
+ this.meta = null
+ this.episodes = null
+
+ this.createdAt = null
+ this.updatedAt = null
+
+ if (feed) {
+ this.construct(feed)
+ }
+ }
+
+ construct(feed) {
+ this.id = feed.id
+ this.slug = feed.slug
+ this.userId = feed.userId
+ this.entityType = feed.entityType
+ this.entityId = feed.entityId
+ this.coverPath = feed.coverPath
+ this.serverAddress = feed.serverAddress
+ this.feedUrl = feed.feedUrl
+ this.meta = new FeedMeta(feed.meta)
+ this.episodes = feed.episodes.map(ep => new FeedEpisode(ep))
+ this.createdAt = feed.createdAt
+ this.updatedAt = feed.updatedAt
+ }
+
+ toJSON() {
+ return {
+ id: this.id,
+ slug: this.slug,
+ userId: this.userId,
+ entityType: this.entityType,
+ entityId: this.entityId,
+ coverPath: this.coverPath,
+ serverAddress: this.serverAddress,
+ feedUrl: this.feedUrl,
+ meta: this.meta.toJSON(),
+ episodes: this.episodes.map(ep => ep.toJSON()),
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt
+ }
+ }
+
+ getEpisodePath(id) {
+ var episode = this.episodes.find(ep => ep.id === id)
+ if (!episode) return null
+ return episode.fullPath
+ }
+
+ setFromItem(userId, slug, libraryItem, serverAddress) {
+ const media = libraryItem.media
+ const mediaMetadata = media.metadata
+ const isPodcast = libraryItem.mediaType === 'podcast'
+
+ const feedUrl = `${serverAddress}/feed/${slug}`
+ const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
+
+ this.id = slug
+ this.slug = slug
+ this.userId = userId
+ this.entityType = 'item'
+ this.entityId = libraryItem.id
+ this.coverPath = media.coverPath || null
+ this.serverAddress = serverAddress
+ this.feedUrl = feedUrl
+
+ this.meta = new FeedMeta()
+ this.meta.title = mediaMetadata.title
+ this.meta.description = mediaMetadata.description
+ this.meta.author = author
+ this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`
+ this.meta.feedUrl = feedUrl
+ this.meta.link = `${serverAddress}/items/${libraryItem.id}`
+
+ this.episodes = []
+ if (isPodcast) { // PODCAST EPISODES
+ media.episodes.forEach((episode) => {
+ var feedEpisode = new FeedEpisode()
+ feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta)
+ this.episodes.push(feedEpisode)
+ })
+ } else { // AUDIOBOOK EPISODES
+ media.tracks.forEach((audioTrack) => {
+ var feedEpisode = new FeedEpisode()
+ feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta)
+ this.episodes.push(feedEpisode)
+ })
+ }
+
+ this.createdAt = Date.now()
+ this.updatedAt = Date.now()
+ }
+
+ buildXml() {
+ const pod = new Podcast(this.meta.getPodcastMeta())
+ this.episodes.forEach((ep) => {
+ pod.addItem(ep.getPodcastEpisode())
+ })
+ return pod.buildXml()
+ }
+}
+module.exports = Feed
\ No newline at end of file
diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js
new file mode 100644
index 00000000..86b66623
--- /dev/null
+++ b/server/objects/FeedEpisode.js
@@ -0,0 +1,130 @@
+const Path = require('path')
+const date = require('date-and-time')
+
+class FeedEpisode {
+ constructor(episode) {
+ this.id = null
+
+ this.title = null
+ this.description = null
+ this.enclosure = null
+ this.pubDate = null
+ this.link = null
+ this.author = null
+ this.explicit = null
+ this.duration = null
+
+ this.libraryItemId = null
+ this.episodeId = null
+ this.trackIndex = null
+ this.fullPath = null
+
+ if (episode) {
+ this.construct(episode)
+ }
+ }
+
+ construct(episode) {
+ this.id = episode.id
+ this.title = episode.title
+ this.description = episode.description
+ this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
+ this.pubDate = episode.pubDate
+ this.link = episode.link
+ this.author = episode.author
+ this.explicit = episode.explicit
+ this.duration = episode.duration
+ this.libraryItemId = episode.libraryItemId
+ this.episodeId = episode.episodeId || null
+ this.trackIndex = episode.trackIndex || 0
+ this.fullPath = episode.fullPath
+ }
+
+ toJSON() {
+ return {
+ id: this.id,
+ title: this.title,
+ description: this.description,
+ enclosure: this.enclosure ? { ...this.enclosure } : null,
+ pubDate: this.pubDate,
+ link: this.link,
+ author: this.author,
+ explicit: this.explicit,
+ duration: this.duration,
+ libraryItemId: this.libraryItemId,
+ episodeId: this.episodeId,
+ trackIndex: this.trackIndex,
+ fullPath: this.fullPath
+ }
+ }
+
+ setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, meta) {
+ const contentUrl = `/feed/${slug}/item/${episode.id}/${episode.audioFile.metadata.filename}`
+ const media = libraryItem.media
+ const mediaMetadata = media.metadata
+
+ this.id = episode.id
+ this.title = episode.title
+ this.description = episode.description || ''
+ this.enclosure = {
+ url: `${serverAddress}${contentUrl}`,
+ type: episode.audioTrack.mimeType,
+ size: episode.size
+ }
+ this.pubDate = episode.pubDate
+ this.link = meta.link
+ this.author = meta.author
+ this.explicit = mediaMetadata.explicit
+ this.duration = episode.duration
+ this.libraryItemId = libraryItem.id
+ this.episodeId = episode.id
+ this.trackIndex = 0
+ this.fullPath = episode.audioFile.metadata.path
+ }
+
+ setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
+ // Example:
Fri, 04 Feb 2015 00:00:00 GMT
+ const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
+
+ const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
+ const media = libraryItem.media
+ const mediaMetadata = media.metadata
+
+ var title = audioTrack.title
+ if (libraryItem.media.chapters.length) {
+ // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
+ var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
+ if (matchingChapter && matchingChapter.title) title = matchingChapter.title
+ }
+
+ this.id = String(audioTrack.index)
+ this.title = title
+ this.description = mediaMetadata.description || ''
+ this.enclosure = {
+ url: `${serverAddress}${contentUrl}`,
+ type: audioTrack.mimeType,
+ size: audioTrack.metadata.size
+ }
+ this.pubDate = audiobookPubDate
+ this.link = meta.link
+ this.author = meta.author
+ this.explicit = mediaMetadata.explicit
+ this.duration = audioTrack.duration
+ this.libraryItemId = libraryItem.id
+ this.episodeId = null
+ this.trackIndex = audioTrack.index
+ this.fullPath = audioTrack.metadata.path
+ }
+
+ getPodcastEpisode() {
+ return {
+ title: this.title,
+ description: this.description || '',
+ enclosure: this.enclosure,
+ date: this.pubDate || '',
+ url: this.link,
+ author: this.author
+ }
+ }
+}
+module.exports = FeedEpisode
\ No newline at end of file
diff --git a/server/objects/FeedMeta.js b/server/objects/FeedMeta.js
new file mode 100644
index 00000000..14dc2756
--- /dev/null
+++ b/server/objects/FeedMeta.js
@@ -0,0 +1,47 @@
+class FeedMeta {
+ constructor(meta) {
+ this.title = null
+ this.description = null
+ this.author = null
+ this.imageUrl = null
+ this.feedUrl = null
+ this.link = null
+
+ if (meta) {
+ this.construct(meta)
+ }
+ }
+
+ construct(meta) {
+ this.title = meta.title
+ this.description = meta.description
+ this.author = meta.author
+ this.imageUrl = meta.imageUrl
+ this.feedUrl = meta.feedUrl
+ this.link = meta.link
+ }
+
+ toJSON() {
+ return {
+ title: this.title,
+ description: this.description,
+ author: this.author,
+ imageUrl: this.imageUrl,
+ feedUrl: this.feedUrl,
+ link: this.link
+ }
+ }
+
+ getPodcastMeta() {
+ return {
+ title: this.title,
+ description: this.description,
+ feedUrl: this.feedUrl,
+ siteUrl: this.link,
+ imageUrl: this.imageUrl,
+ author: this.author || 'advplyr',
+ language: 'en'
+ }
+ }
+}
+module.exports = FeedMeta
\ No newline at end of file