From 6db75f8a66c02540e2bf82ea73dea439f18813e5 Mon Sep 17 00:00:00 2001 From: Thomas Zarebczan Date: Fri, 6 Aug 2021 16:53:01 -0400 Subject: [PATCH] rss tests Various fixes and improvements to work with Apple Podcasts f f image support --- ui/util/web.js | 10 +++++++-- web/src/rss.js | 56 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/ui/util/web.js b/ui/util/web.js index e6f284358..9f4703ae9 100644 --- a/ui/util/web.js +++ b/ui/util/web.js @@ -3,7 +3,10 @@ const { URL, LBRY_WEB_STREAMING_API } = require('../../config'); const CONTINENT_COOKIE = 'continent'; function generateStreamUrl(claimName, claimId) { - return `${LBRY_WEB_STREAMING_API}/content/claims/${claimName}/${claimId}/stream`; + return `${LBRY_WEB_STREAMING_API}/content/claims/${encodeURIComponent(claimName) + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29')}/${claimId}/stream`; } function generateEmbedUrl(claimName, claimId, includeStartTime, startTime, referralLink) { @@ -16,7 +19,10 @@ function generateEmbedUrl(claimName, claimId, includeStartTime, startTime, refer urlParams.append('r', referralLink); } - return `${URL}/$/embed/${encodeURIComponent(claimName).replace(/'/g, '%27')}/${claimId}?${urlParams.toString()}`; + return `${URL}/$/embed/${encodeURIComponent(claimName) + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29')}/${claimId}?${urlParams.toString()}`; } function generateDownloadUrl(claimName, claimId) { diff --git a/web/src/rss.js b/web/src/rss.js index 714d9a448..1125cdd5f 100644 --- a/web/src/rss.js +++ b/web/src/rss.js @@ -2,6 +2,7 @@ const { generateStreamUrl } = require('../../ui/util/web'); const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js'); const { Lbry } = require('lbry-redux'); const Rss = require('rss'); +const Mime = require('mime-types'); const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`; const proxyURL = `${SDK_API_PATH}/proxy`; @@ -46,7 +47,7 @@ async function getClaimsFromChannel(claimId, count) { page_size: count, has_source: true, claim_type: 'stream', - order_by: ['creation_timestamp'], + order_by: ['release_time'], no_totals: true, }; @@ -57,16 +58,39 @@ async function getClaimsFromChannel(claimId, count) { // Helpers // **************************************************************************** +function encodeWithSpecialCharEncode(string) { + // encodeURIComponent doesn't encode `'` and others + // which other services may not like + return encodeURIComponent(string).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'); +} + const generateEnclosureForClaimContent = (claim) => { const value = claim.value; if (!value || !value.stream_type) { return undefined; } + const fileExt = value.source && value.source.media_type && '.' + Mime.extension(value.source.media_type); switch (value.stream_type) { case 'video': + return { + url: generateStreamUrl(claim.name, claim.claim_id) + (fileExt || '.mp4'), + type: (value.source && value.source.media_type) || 'video/mp4', + size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback. + }; + case 'audio': + return { + url: generateStreamUrl(claim.name, claim.claim_id) + (fileExt || '.mp3'), + type: (value.source && value.source.media_type) || 'audio/mpeg', + size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback. + }; case 'image': + return { + url: generateStreamUrl(claim.name, claim.claim_id) + (fileExt || '.jpeg'), + type: (value.source && value.source.media_type) || 'image/jpeg', + size: (value.source && value.source.size) || 0, // Per spec, 0 is a valid fallback. + }; case 'document': case 'software': return { @@ -102,11 +126,10 @@ const isEmailRoughlyValid = (email) => /^\S+@\S+$/.test(email); * @returns any */ const generateItunesOwnerElement = (claim) => { - let name = '---'; let email = 'no-reply@odysee.com'; + let name = claim && (claim.value && claim.value.title ? claim.value.title : claim.name); if (claim && claim.value) { - name = claim.name; if (isEmailRoughlyValid(claim.value.email)) { email = claim.value.email; } @@ -118,7 +141,7 @@ const generateItunesOwnerElement = (claim) => { }; const generateItunesExplicitElement = (claim) => { - const tags = (claim && claim.value && claim.tags) || []; + const tags = (claim && claim.value && claim.value.tags) || []; return { 'itunes:explicit': tags.includes('mature') ? 'yes' : 'no' }; }; @@ -145,15 +168,16 @@ const getItunesCategory = (claim) => { 'TV & Film', ]; - const tags = (claim && claim.value && claim.tags) || []; - for (let i = 0; i < tags.length; ++i) { - const tag = tags[i]; - if (itunesCategories.includes(tag)) { + const tags = (claim && claim.value && claim.value.tags) || []; + + for (let i = 0; i < itunesCategories.length; ++i) { + const itunesCategory = itunesCategories[i]; + if (tags.includes(itunesCategory.toLowerCase())) { // "Note: Although you can specify more than one category and subcategory // in your RSS feed, Apple Podcasts only recognizes the first category and // subcategory." // --> The only parse the first found tag. - return tag.replace('&', '&'); + return itunesCategory.replace('&', '&'); } } @@ -197,8 +221,9 @@ const getFormattedDescription = (claim) => { function generateFeed(feedLink, channelClaim, claimsInChannel) { // --- Channel --- + let channelTitle = (channelClaim.value && channelClaim.value.title) || channelClaim.name; const feed = new Rss({ - title: ((channelClaim.value && channelClaim.value.title) || channelClaim.name) + ' on ' + SITE_NAME, + title: channelTitle + ' on ' + SITE_NAME, description: getFormattedDescription(channelClaim), feed_url: feedLink, site_url: URL, @@ -206,7 +231,7 @@ function generateFeed(feedLink, channelClaim, claimsInChannel) { language: getLanguageValue(channelClaim), custom_namespaces: { itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd' }, custom_elements: [ - { 'itunes:author': channelClaim.name }, + { 'itunes:author': channelTitle }, { 'itunes:category': [ { @@ -231,17 +256,20 @@ function generateFeed(feedLink, channelClaim, claimsInChannel) { : ''; const description = thumbnailHtml + getFormattedDescription(c); + const url = `${URL}/${encodeWithSpecialCharEncode(c.name)}:${c.claim_id}`; + const date = c.release_time ? c.release_time * 1000 : c.meta && c.meta.creation_timestamp * 1000; + feed.item({ title: title, description: description, - url: `${URL}/${c.name}:${c.claim_id}`, + url: url, guid: undefined, // defaults to 'url' author: undefined, // defaults feed author property - date: new Date(c.meta ? c.meta.creation_timestamp * 1000 : null), + date: new Date(date), enclosure: generateEnclosureForClaimContent(c), custom_elements: [ { 'itunes:title': title }, - { 'itunes:author': channelClaim.name }, + { 'itunes:author': channelTitle }, generateItunesImageElement(c), generateItunesDurationElement(c), generateItunesExplicitElement(c),