From 03bffb725a53b503fa777865c2f83ad208f41c31 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 7 Jun 2022 19:25:14 -0500 Subject: [PATCH] Update:Remove rss feed dependencies add node-xml lib --- package-lock.json | 83 +-------- package.json | 3 +- server/libs/rss/index.js | 193 ++++++++++++++++++++ server/libs/xml/escapeForXML.js | 17 ++ server/libs/xml/index.js | 286 ++++++++++++++++++++++++++++++ server/managers/RssFeedManager.js | 7 +- server/objects/Feed.js | 9 +- server/objects/FeedEpisode.js | 18 +- server/objects/FeedMeta.js | 45 ++++- 9 files changed, 556 insertions(+), 105 deletions(-) create mode 100644 server/libs/rss/index.js create mode 100644 server/libs/xml/escapeForXML.js create mode 100644 server/libs/xml/index.js diff --git a/package-lock.json b/package-lock.json index 78464f19..d17584c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "requires": true, "packages": { "": { - "version": "2.0.17", + "version": "2.0.20", "license": "GPL-3.0", "dependencies": { "archiver": "^5.3.0", @@ -26,7 +26,6 @@ "node-cron": "^3.0.0", "node-ffprobe": "^3.0.0", "node-stream-zip": "^1.15.0", - "podcast": "^2.0.0", "proper-lockfile": "^4.1.2", "read-chunk": "^3.1.0", "recursive-readdir-async": "^1.1.8", @@ -1560,14 +1559,6 @@ "node": ">=6" } }, - "node_modules/podcast": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz", - "integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==", - "dependencies": { - "rss": "^1.2.2" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1713,34 +1704,6 @@ "node": ">= 4" } }, - "node_modules/rss": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", - "integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=", - "dependencies": { - "mime-types": "2.1.13", - "xml": "1.0.1" - } - }, - "node_modules/rss/node_modules/mime-db": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", - "integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/rss/node_modules/mime-types": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", - "integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=", - "dependencies": { - "mime-db": "~1.25.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2072,11 +2035,6 @@ } } }, - "node_modules/xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" - }, "node_modules/xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", @@ -3271,14 +3229,6 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, - "podcast": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz", - "integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==", - "requires": { - "rss": "^1.2.2" - } - }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -3391,30 +3341,6 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, - "rss": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", - "integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=", - "requires": { - "mime-types": "2.1.13", - "xml": "1.0.1" - }, - "dependencies": { - "mime-db": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", - "integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=" - }, - "mime-types": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", - "integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=", - "requires": { - "mime-db": "~1.25.0" - } - } - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3654,11 +3580,6 @@ "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "requires": {} }, - "xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" - }, "xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", @@ -3684,4 +3605,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4ea8a752..c1a7c6c9 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,10 @@ "node-cron": "^3.0.0", "node-ffprobe": "^3.0.0", "node-stream-zip": "^1.15.0", - "podcast": "^2.0.0", "proper-lockfile": "^4.1.2", "read-chunk": "^3.1.0", "recursive-readdir-async": "^1.1.8", "socket.io": "^4.4.1", "xml2js": "^0.4.23" } -} \ No newline at end of file +} diff --git a/server/libs/rss/index.js b/server/libs/rss/index.js new file mode 100644 index 00000000..09c05968 --- /dev/null +++ b/server/libs/rss/index.js @@ -0,0 +1,193 @@ +// node-rss +// SOURCE: https://github.com/dylang/node-rss +// LICENSE: https://creativecommons.org/licenses/by-sa/4.0/ + +'use strict'; + +var mime = require('mime-types'); +var xml = require('../xml'); +var fs = require('fs'); + + +function ifTruePush(bool, array, data) { + if (bool) { + array.push(data); + } +} + +function ifTruePushArray(bool, array, dataArray) { + if (!bool) { + return; + } + + dataArray.forEach(function (item) { + ifTruePush(item, array, item); + }); +} + +function getSize(filename) { + if (typeof fs === 'undefined') { + return 0; + } + return fs.statSync(filename).size; +} + +function generateXML(data) { + + var channel = []; + channel.push({ title: { _cdata: data.title } }); + channel.push({ description: { _cdata: data.description || data.title } }); + channel.push({ link: data.site_url || 'http://github.com/dylang/node-rss' }); + // image_url set? + if (data.image_url) { + channel.push({ image: [{ url: data.image_url }, { title: data.title }, { link: data.site_url }] }); + } + channel.push({ generator: data.generator }); + channel.push({ lastBuildDate: new Date().toUTCString() }); + + ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } }); + ifTruePush(data.author, channel, { 'author': { _cdata: data.author } }); + ifTruePush(data.pubDate, channel, { 'pubDate': new Date(data.pubDate).toGMTString() }); + ifTruePush(data.copyright, channel, { 'copyright': { _cdata: data.copyright } }); + ifTruePush(data.language, channel, { 'language': { _cdata: data.language } }); + ifTruePush(data.managingEditor, channel, { 'managingEditor': { _cdata: data.managingEditor } }); + ifTruePush(data.webMaster, channel, { 'webMaster': { _cdata: data.webMaster } }); + ifTruePush(data.docs, channel, { 'docs': data.docs }); + ifTruePush(data.ttl, channel, { 'ttl': data.ttl }); + ifTruePush(data.hub, channel, { 'atom:link': { _attr: { href: data.hub, rel: 'hub' } } }); + + if (data.categories) { + data.categories.forEach(function (category) { + ifTruePush(category, channel, { category: { _cdata: category } }); + }); + } + + ifTruePushArray(data.custom_elements, channel, data.custom_elements); + + data.items.forEach(function (item) { + var item_values = [ + { title: { _cdata: item.title } } + ]; + ifTruePush(item.description, item_values, { description: { _cdata: item.description } }); + ifTruePush(item.url, item_values, { link: item.url }); + ifTruePush(item.link || item.guid || item.title, item_values, { guid: [{ _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title] }); + + item.categories.forEach(function (category) { + ifTruePush(category, item_values, { category: { _cdata: category } }); + }); + + ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } }); + ifTruePush(item.date, item_values, { pubDate: new Date(item.date).toGMTString() }); + + //Set GeoRSS to true if lat and long are set + data.geoRSS = data.geoRSS || (item.lat && item.long); + ifTruePush(item.lat, item_values, { 'geo:lat': item.lat }); + ifTruePush(item.long, item_values, { 'geo:long': item.long }); + + if (item.enclosure && item.enclosure.url) { + if (item.enclosure.file) { + item_values.push({ + enclosure: { + _attr: { + url: item.enclosure.url, + length: item.enclosure.size || getSize(item.enclosure.file), + type: item.enclosure.type || mime.lookup(item.enclosure.file) + } + } + }); + } else { + item_values.push({ + enclosure: { + _attr: { + url: item.enclosure.url, + length: item.enclosure.size || 0, + type: item.enclosure.type || mime.lookup(item.enclosure.url) + } + } + }); + } + } + + ifTruePushArray(item.custom_elements, item_values, item.custom_elements); + + channel.push({ item: item_values }); + + }); + + //set up the attributes for the RSS feed. + var _attr = { + 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', + 'xmlns:content': 'http://purl.org/rss/1.0/modules/content/', + 'xmlns:atom': 'http://www.w3.org/2005/Atom', + version: '2.0' + }; + + Object.keys(data.custom_namespaces).forEach(function (name) { + _attr['xmlns:' + name] = data.custom_namespaces[name]; + }); + + //only add namespace if GeoRSS is true + if (data.geoRSS) { + _attr['xmlns:geo'] = 'http://www.w3.org/2003/01/geo/wgs84_pos#'; + } + + return { + rss: [ + { _attr: _attr }, + { channel: channel } + ] + }; +} + +function RSS(options, items) { + options = options || {}; + + this.title = options.title || 'Untitled RSS Feed'; + this.description = options.description || ''; + this.generator = options.generator || 'RSS for Node'; + this.feed_url = options.feed_url; + this.site_url = options.site_url; + this.image_url = options.image_url; + this.author = options.author; + this.categories = options.categories; + this.pubDate = options.pubDate; + this.hub = options.hub; + this.docs = options.docs; + this.copyright = options.copyright; + this.language = options.language; + this.managingEditor = options.managingEditor; + this.webMaster = options.webMaster; + this.ttl = options.ttl; + //option to return feed as GeoRSS is set automatically if feed.lat/long is used + this.geoRSS = options.geoRSS || false; + this.custom_namespaces = options.custom_namespaces || {}; + this.custom_elements = options.custom_elements || []; + this.items = items || []; + + this.item = function (options) { + options = options || {}; + var item = { + title: options.title || 'No title', + description: options.description || '', + url: options.url, + guid: options.guid, + categories: options.categories || [], + author: options.author, + date: options.date, + lat: options.lat, + long: options.long, + enclosure: options.enclosure || false, + custom_elements: options.custom_elements || [] + }; + + this.items.push(item); + return this; + }; + + this.xml = function (indent) { + return '' + + xml(generateXML(this), indent); + }; +} + +module.exports = RSS; \ No newline at end of file diff --git a/server/libs/xml/escapeForXML.js b/server/libs/xml/escapeForXML.js new file mode 100644 index 00000000..5fbd0fb9 --- /dev/null +++ b/server/libs/xml/escapeForXML.js @@ -0,0 +1,17 @@ +var XML_CHARACTER_MAP = { + '&': '&', + '"': '"', + "'": ''', + '<': '<', + '>': '>' +}; + +function escapeForXML(string) { + return string && string.replace + ? string.replace(/([&"<>'])/g, function (str, item) { + return XML_CHARACTER_MAP[item]; + }) + : string; +} + +module.exports = escapeForXML; \ No newline at end of file diff --git a/server/libs/xml/index.js b/server/libs/xml/index.js new file mode 100644 index 00000000..207c390e --- /dev/null +++ b/server/libs/xml/index.js @@ -0,0 +1,286 @@ +// node-xml +// SOURCE: https://github.com/dylang/node-xml +// LICENSE: https://github.com/dylang/node-xml/blob/master/LICENSE + +var escapeForXML = require('./escapeForXML'); +var Stream = require('stream').Stream; + +var DEFAULT_INDENT = ' '; + +function xml(input, options) { + + if (typeof options !== 'object') { + options = { + indent: options + }; + } + + var stream = options.stream ? new Stream() : null, + output = "", + interrupted = false, + indent = !options.indent ? '' + : options.indent === true ? DEFAULT_INDENT + : options.indent, + instant = true; + + + function delay(func) { + if (!instant) { + func(); + } else { + process.nextTick(func); + } + } + + function append(interrupt, out) { + if (out !== undefined) { + output += out; + } + if (interrupt && !interrupted) { + stream = stream || new Stream(); + interrupted = true; + } + if (interrupt && interrupted) { + var data = output; + delay(function () { stream.emit('data', data) }); + output = ""; + } + } + + function add(value, last) { + format(append, resolve(value, indent, indent ? 1 : 0), last); + } + + function end() { + if (stream) { + var data = output; + delay(function () { + stream.emit('data', data); + stream.emit('end'); + stream.readable = false; + stream.emit('close'); + }); + } + } + + function addXmlDeclaration(declaration) { + var encoding = declaration.encoding || 'UTF-8', + attr = { version: '1.0', encoding: encoding }; + + if (declaration.standalone) { + attr.standalone = declaration.standalone + } + + add({ '?xml': { _attr: attr } }); + output = output.replace('/>', '?>'); + } + + // disable delay delayed + delay(function () { instant = false }); + + if (options.declaration) { + addXmlDeclaration(options.declaration); + } + + if (input && input.forEach) { + input.forEach(function (value, i) { + var last; + if (i + 1 === input.length) + last = end; + add(value, last); + }); + } else { + add(input, end); + } + + if (stream) { + stream.readable = true; + return stream; + } + return output; +} + +function element(/*input, …*/) { + var input = Array.prototype.slice.call(arguments), + self = { + _elem: resolve(input) + }; + + self.push = function (input) { + if (!this.append) { + throw new Error("not assigned to a parent!"); + } + var that = this; + var indent = this._elem.indent; + format(this.append, resolve( + input, indent, this._elem.icount + (indent ? 1 : 0)), + function () { that.append(true) }); + }; + + self.close = function (input) { + if (input !== undefined) { + this.push(input); + } + if (this.end) { + this.end(); + } + }; + + return self; +} + +function create_indent(character, count) { + return (new Array(count || 0).join(character || '')) +} + +function resolve(data, indent, indent_count) { + indent_count = indent_count || 0; + var indent_spaces = create_indent(indent, indent_count); + var name; + var values = data; + var interrupt = false; + + if (typeof data === 'object') { + var keys = Object.keys(data); + name = keys[0]; + values = data[name]; + + if (values && values._elem) { + values._elem.name = name; + values._elem.icount = indent_count; + values._elem.indent = indent; + values._elem.indents = indent_spaces; + values._elem.interrupt = values; + return values._elem; + } + } + + var attributes = [], + content = []; + + var isStringContent; + + function get_attributes(obj) { + var keys = Object.keys(obj); + keys.forEach(function (key) { + attributes.push(attribute(key, obj[key])); + }); + } + + switch (typeof values) { + case 'object': + if (values === null) break; + + if (values._attr) { + get_attributes(values._attr); + } + + if (values._cdata) { + content.push( + ('/g, ']]]]>') + ']]>' + ); + } + + if (values.forEach) { + isStringContent = false; + content.push(''); + values.forEach(function (value) { + if (typeof value == 'object') { + var _name = Object.keys(value)[0]; + + if (_name == '_attr') { + get_attributes(value._attr); + } else { + content.push(resolve( + value, indent, indent_count + 1)); + } + } else { + //string + content.pop(); + isStringContent = true; + content.push(escapeForXML(value)); + } + + }); + if (!isStringContent) { + content.push(''); + } + } + break; + + default: + //string + content.push(escapeForXML(values)); + + } + + return { + name: name, + interrupt: interrupt, + attributes: attributes, + content: content, + icount: indent_count, + indents: indent_spaces, + indent: indent + }; +} + +function format(append, elem, end) { + + if (typeof elem != 'object') { + return append(false, elem); + } + + var len = elem.interrupt ? 1 : elem.content.length; + + function proceed() { + while (elem.content.length) { + var value = elem.content.shift(); + + if (value === undefined) continue; + if (interrupt(value)) return; + + format(append, value); + } + + append(false, (len > 1 ? elem.indents : '') + + (elem.name ? '' : '') + + (elem.indent && !end ? '\n' : '')); + + if (end) { + end(); + } + } + + function interrupt(value) { + if (value.interrupt) { + value.interrupt.append = append; + value.interrupt.end = proceed; + value.interrupt = false; + append(true); + return true; + } + return false; + } + + append(false, elem.indents + + (elem.name ? '<' + elem.name : '') + + (elem.attributes.length ? ' ' + elem.attributes.join(' ') : '') + + (len ? (elem.name ? '>' : '') : (elem.name ? '/>' : '')) + + (elem.indent && len > 1 ? '\n' : '')); + + if (!len) { + return append(false, elem.indent ? '\n' : ''); + } + + if (!interrupt(elem)) { + proceed(); + } +} + +function attribute(key, value) { + return key + '=' + '"' + escapeForXML(value) + '"'; +} + +module.exports = xml; +module.exports.element = module.exports.Element = element; \ No newline at end of file diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index e6ed676b..6c361445 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -1,7 +1,5 @@ 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') @@ -35,7 +33,7 @@ class RssFeedManager { res.sendStatus(404) return } - // var xml = feedData.feed.buildXml() + var xml = feed.buildXml() res.set('Content-Type', 'text/xml') res.send(xml) @@ -55,9 +53,6 @@ class RssFeedManager { return } res.sendFile(episodePath) - // var remainingPath = req.params['0'] - // var fullPath = Path.join(feedData.libraryItemPath, remainingPath) - // res.sendFile(fullPath) } getFeedCover(req, res) { diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 62f4da44..095b0019 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -1,6 +1,6 @@ const FeedMeta = require('./FeedMeta') const FeedEpisode = require('./FeedEpisode') -const { Podcast } = require('podcast') +const RSS = require('../libs/rss') class Feed { constructor(feed) { @@ -90,6 +90,7 @@ class Feed { this.meta.imageUrl = media.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png` this.meta.feedUrl = feedUrl this.meta.link = `${serverAddress}/items/${libraryItem.id}` + this.meta.explicit = !!mediaMetadata.explicit this.episodes = [] if (isPodcast) { // PODCAST EPISODES @@ -113,11 +114,11 @@ class Feed { buildXml() { if (this.xml) return this.xml - const pod = new Podcast(this.meta.getPodcastMeta()) + var rssfeed = new RSS(this.meta.getRSSData()) this.episodes.forEach((ep) => { - pod.addItem(ep.getPodcastEpisode()) + rssfeed.item(ep.getRSSData()) }) - this.xml = pod.buildXml() + this.xml = rssfeed.xml() return this.xml } } diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index 86b66623..04fe0734 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -1,5 +1,6 @@ const Path = require('path') const date = require('date-and-time') +const { secondsToTimestamp } = require('../utils/index') class FeedEpisode { constructor(episode) { @@ -116,14 +117,23 @@ class FeedEpisode { this.fullPath = audioTrack.metadata.path } - getPodcastEpisode() { + getRSSData() { return { title: this.title, description: this.description || '', - enclosure: this.enclosure, - date: this.pubDate || '', url: this.link, - author: this.author + guid: this.enclosure.url, + author: this.author, + date: this.pubDate, + enclosure: this.enclosure, + custom_elements: [ + { 'itunes:author': this.author }, + { 'itunes:duration': secondsToTimestamp(this.duration) }, + { 'itunes:summary': this.description || '' }, + { + "itunes:explicit": !!this.explicit + } + ] } } } diff --git a/server/objects/FeedMeta.js b/server/objects/FeedMeta.js index 14dc2756..6c029095 100644 --- a/server/objects/FeedMeta.js +++ b/server/objects/FeedMeta.js @@ -6,6 +6,7 @@ class FeedMeta { this.imageUrl = null this.feedUrl = null this.link = null + this.explicit = null if (meta) { this.construct(meta) @@ -19,6 +20,7 @@ class FeedMeta { this.imageUrl = meta.imageUrl this.feedUrl = meta.feedUrl this.link = meta.link + this.explicit = meta.explicit } toJSON() { @@ -28,19 +30,46 @@ class FeedMeta { author: this.author, imageUrl: this.imageUrl, feedUrl: this.feedUrl, - link: this.link + link: this.link, + explicit: this.explicit } } - getPodcastMeta() { + getRSSData() { return { title: this.title, - description: this.description, - feedUrl: this.feedUrl, - siteUrl: this.link, - imageUrl: this.imageUrl, - author: this.author || 'advplyr', - language: 'en' + description: this.description || '', + generator: 'Audiobookshelf', + feed_url: this.feedUrl, + site_url: this.link, + image_url: this.imageUrl, + language: 'en', + custom_namespaces: { + 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', + 'psc': 'http://podlove.org/simple-chapters', + 'podcast': 'https://podcastindex.org/namespace/1.0' + }, + custom_elements: [ + { 'author': this.author || 'advplyr' }, + { 'itunes:author': this.author || 'advplyr' }, + { 'itunes:summary': this.description || '' }, + { + 'itunes:image': { + _attr: { + href: this.imageUrl + } + } + }, + { + 'itunes:owner': [ + { 'itunes:name': this.author || '' }, + { 'itunes:email': '' } + ] + }, + { + "itunes:explicit": !!this.explicit + } + ] } } }