Add:Persist RSS feeds in db #696, Update:RSS feed data model

This commit is contained in:
advplyr 2022-06-07 18:29:43 -05:00
parent 881baa818d
commit fbbceaa642
9 changed files with 366 additions and 130 deletions

118
server/objects/Feed.js Normal file
View file

@ -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

View file

@ -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: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
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

View file

@ -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