-
-
Notifications
You must be signed in to change notification settings - Fork 12
/
jsonfeed-to-rss-object.js
182 lines (169 loc) · 7.75 KB
/
jsonfeed-to-rss-object.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
const packageInfo = require('./package.json')
const generateTitle = require('./lib/generate-title')
const get = require('lodash.get')
const cleanDeep = require('clean-deep')
const striptags = require('striptags')
const { cleanCategory, cleanSubcategory } = require('./lib/clean-category')
const { getSubtitle, getSummary, truncate4k, truncate250, secondsToHMS, getPodcastType } = require('./lib/itunes-fields')
const existy = require('existy')
const truthy = require('@bret/truthy')
const merge = require('lodash.merge')
module.exports = function jsonfeedToAtomObject (jf, opts) {
const now = new Date()
opts = Object.assign({
feedURLFn: (feedURL, jf) => feedURL.replace(/\.json\b/, '-rss.xml'),
language: 'en-us',
copyright: `© ${now.getFullYear()} ${jf.author && jf.author.name ? jf.author.name : ''}`,
managingEditor: null, // [email protected] (First Last)
webMaster: null, // [email protected] (First Last)
idIsPermalink: false, // guid is permalink
categories: null, // site level categories. no mapping, so leave as option array.
ttl: null,
skipHours: null, // array of hour numbers
skipDays: null, // array of skip days
itunes: !!jf._itunes // enable/disable itunes tags
}, opts)
if (typeof opts.itunes === 'object') {
jf = merge({}, jf)
jf._itunes = merge(jf._itunes, opts.itunes)
}
// 2.0.11 http://www.rssboard.org/rss-specification
// best practice http://www.rssboard.org/rss-profile
// JSON Feed to rss mapping based off http://cyber.harvard.edu/rss/rss.html
// and http://www.rssboard.org/rss-profile and
// https://validator.w3.org/feed/docs/rss2.html
const { title, version, home_page_url: homePageURL, description, feed_url: feedURL } = jf
if (version !== 'https://jsonfeed.org/version/1') throw new Error('jsonfeed-to-atom: JSON feed version 1 required')
if (!title) throw new Error('jsonfeed-to-rss: missing title')
if (!feedURL) throw new Error('jsonfeed-to-atom: missing feed_url')
if (!homePageURL) throw new Error('jsonfeed-to-rss: JSON feed missing home_page_url property')
const rssFeedURL = opts.feedURLFn(feedURL, jf)
const rssTitle = `${title}`
const rss = {
'atom:link': {
'@href': rssFeedURL,
'@rel': 'self',
'@type': 'application/rss+xml'
},
title: rssTitle,
link: homePageURL,
description,
language: opts.language,
copyright: opts.copyright,
managingEditor: opts.managingEditor,
webMaster: opts.webMaster,
pubDate: now.toUTCString(), // override with the newest pubdate thats less than now
// lastBuildDate: now.toUTCString(),
category: (opts.itunes && !opts.category) ? [get(jf, '_itunes.category'), get(jf, '_itunes.subcategory')] : opts.category,
generator: `${packageInfo.name} ${packageInfo.version} (${packageInfo.homepage})`,
docs: 'http://www.rssboard.org/rss-specification',
// TODO: cloud
ttl: opts.ttl,
image: jf.icon
? {
url: jf.icon,
link: homePageURL,
title: rssTitle
}
: undefined,
skipHours: opts.skipHours ? { hour: opts.skipHours } : null,
skipDays: opts.skipDays ? { day: opts.skipDays } : null
}
if (opts.itunes) {
const category = get(jf, '_itunes.category') || get(opts, 'category[0]')
const subcategory = get(jf, '_itunes.subcategory') || get(opts, 'category[1]')
Object.assign(rss, {
'itunes:author': get(jf, '_itunes.author') || get(jf, 'author.name'),
'itunes:summary': getSummary(jf),
'itunes:subtitle': getSubtitle(jf),
'itunes:type': getPodcastType(jf),
'itunes:owner': {
'itunes:name': get(jf, '_itunes.owner.name') || get(jf, 'author.name'),
'itunes:email': get(jf, '_itunes.owner.email')
},
'itunes:image': {
'@href': get(jf, '_itunes.image') || get(jf, 'icon')
},
'itunes:category': {
'@text': cleanCategory(category),
'itunes:category': {
'@text': cleanSubcategory(category, subcategory)
}
},
'itunes:explicit': existy(get(jf, '_itunes.explicit')) ? truthy(get(jf, '_itunes.explicit')) ? 'yes' : 'no' : null,
'itunes:block': get(jf, '_itunes.block') ? 'Yes' : null,
'itunes:complete': get(jf, '_itunes.complete') ? 'Yes' : null,
'itunes:new-feed-url': get(jf, '_itunes.new_feed_url') ? opts.feedURLFn(get(jf, '_itunes.new_feed_url'), jf) : null,
description: truncate4k(rss.description),
title: truncate250(rss.title)
})
}
if (jf.items) {
let mostRecentlyUpdated = '0'
rss.item = jf.items.map(item => {
// capture mostRecentlyUpdated date, if any
if (item.date_published && (item.date_published > mostRecentlyUpdated)) mostRecentlyUpdated = item.date_published
if (item.date_modified && (item.date_modified > mostRecentlyUpdated)) mostRecentlyUpdated = item.date_modified
// Generate item object
const title = generateTitle(item)
const date = new Date(item.date_published)
const rssItem = {
title,
link: item.external_url || item.url,
// author: getManagingEditor(item) || getManagingEditor(jf),
'dc:creator': get(item, 'author.name') || get(jf, 'author.name'),
// RSS supports HTML in description, but we are only going to use it for plain text, a common practice/misconception + apple recommended.
description: item.content_text || striptags(item.content_html),
'content:encoded': {
'#cdata': item.content_html
},
category: item.tags,
guid: {
'#text': item.id,
'@isPermaLink': opts.idIsPermalink
},
pubDate: date.toUTCString()
}
if (item.attachments && item.attachments.length > 0) {
const attachment = item.attachments[0] // RSS only supports 1 per item!
rssItem.enclosure = {
'@type': attachment.mime_type,
'@url': attachment.url,
'@length': attachment.size_in_bytes
}
if (opts.itunes) {
Object.assign(rssItem, {
'itunes:episodeType': ['full', 'trailer', 'bonus'].some(type => get(item, '_itunes.episode_type') === type) ? get(item, '_itunes.episode_type') : 'full',
'itunes:title': get(item, '_itunes.title') || generateTitle(item),
'itunes:author': get(item, '_itunes.author') || get(item, 'author.name') || get(jf, '_itunes.author') || get(jf, 'author.name'),
'itunes:episode': Number.isInteger(get(item, '_itunes.episode')) ? get(item, '_itunes.episode') : null,
'itunes:subtitle': getSubtitle(item),
'itunes:summary': getSummary(item),
'itunes:image': {
'@href': get(item, '_itunes.image') || get(item, 'image')
},
'itunes:duration': get(item, '_itunes.duration') || existy(attachment.duration_in_seconds) ? secondsToHMS(attachment.duration_in_seconds) : null,
'itunes:season': get(item, '_itunes.season') || null,
'itunes:block': get(item, '_itunes.block') ? 'Yes' : null,
'itunes:explicit': existy(get(item, '_itunes.explicit')) ? truthy(get(item, '_itunes.explicit')) ? 'yes' : 'no' : null,
'itunes:isClosedCaptioned': get(item, '_itunes.is_closed_captioned') ? 'Yes' : null,
description: truncate4k(rssItem.description)
})
}
}
return rssItem
})
// Replace pubdate date most recently updated or published
if (mostRecentlyUpdated > '0') rss.pubDate = new Date(mostRecentlyUpdated).toUTCString()
}
return cleanDeep({
rss: {
'@version': '2.0',
'@xmlns:atom': 'http://www.w3.org/2005/Atom',
'@xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'@xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
'@xmlns:itunes': opts.itunes ? 'http://www.itunes.com/dtds/podcast-1.0.dtd' : null,
channel: rss
}
})
}