rss tests

Various fixes and improvements to work with Apple Podcasts

f

f

image support
This commit is contained in:
Thomas Zarebczan 2021-08-06 16:53:01 -04:00 committed by Thomas Zarebczan
parent 7921c0971e
commit 6db75f8a66
2 changed files with 50 additions and 16 deletions

View file

@ -3,7 +3,10 @@ const { URL, LBRY_WEB_STREAMING_API } = require('../../config');
const CONTINENT_COOKIE = 'continent'; const CONTINENT_COOKIE = 'continent';
function generateStreamUrl(claimName, claimId) { 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) { function generateEmbedUrl(claimName, claimId, includeStartTime, startTime, referralLink) {
@ -16,7 +19,10 @@ function generateEmbedUrl(claimName, claimId, includeStartTime, startTime, refer
urlParams.append('r', referralLink); 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) { function generateDownloadUrl(claimName, claimId) {

View file

@ -2,6 +2,7 @@ const { generateStreamUrl } = require('../../ui/util/web');
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js'); const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
const { Lbry } = require('lbry-redux'); const { Lbry } = require('lbry-redux');
const Rss = require('rss'); const Rss = require('rss');
const Mime = require('mime-types');
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`; const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const proxyURL = `${SDK_API_PATH}/proxy`; const proxyURL = `${SDK_API_PATH}/proxy`;
@ -46,7 +47,7 @@ async function getClaimsFromChannel(claimId, count) {
page_size: count, page_size: count,
has_source: true, has_source: true,
claim_type: 'stream', claim_type: 'stream',
order_by: ['creation_timestamp'], order_by: ['release_time'],
no_totals: true, no_totals: true,
}; };
@ -57,16 +58,39 @@ async function getClaimsFromChannel(claimId, count) {
// Helpers // 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 generateEnclosureForClaimContent = (claim) => {
const value = claim.value; const value = claim.value;
if (!value || !value.stream_type) { if (!value || !value.stream_type) {
return undefined; return undefined;
} }
const fileExt = value.source && value.source.media_type && '.' + Mime.extension(value.source.media_type);
switch (value.stream_type) { switch (value.stream_type) {
case 'video': 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': 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': 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 'document':
case 'software': case 'software':
return { return {
@ -102,11 +126,10 @@ const isEmailRoughlyValid = (email) => /^\S+@\S+$/.test(email);
* @returns any * @returns any
*/ */
const generateItunesOwnerElement = (claim) => { const generateItunesOwnerElement = (claim) => {
let name = '---';
let email = 'no-reply@odysee.com'; let email = 'no-reply@odysee.com';
let name = claim && (claim.value && claim.value.title ? claim.value.title : claim.name);
if (claim && claim.value) { if (claim && claim.value) {
name = claim.name;
if (isEmailRoughlyValid(claim.value.email)) { if (isEmailRoughlyValid(claim.value.email)) {
email = claim.value.email; email = claim.value.email;
} }
@ -118,7 +141,7 @@ const generateItunesOwnerElement = (claim) => {
}; };
const generateItunesExplicitElement = (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' }; return { 'itunes:explicit': tags.includes('mature') ? 'yes' : 'no' };
}; };
@ -145,15 +168,16 @@ const getItunesCategory = (claim) => {
'TV & Film', 'TV & Film',
]; ];
const tags = (claim && claim.value && claim.tags) || []; const tags = (claim && claim.value && claim.value.tags) || [];
for (let i = 0; i < tags.length; ++i) {
const tag = tags[i]; for (let i = 0; i < itunesCategories.length; ++i) {
if (itunesCategories.includes(tag)) { const itunesCategory = itunesCategories[i];
if (tags.includes(itunesCategory.toLowerCase())) {
// "Note: Although you can specify more than one category and subcategory // "Note: Although you can specify more than one category and subcategory
// in your RSS feed, Apple Podcasts only recognizes the first category and // in your RSS feed, Apple Podcasts only recognizes the first category and
// subcategory." // subcategory."
// --> The only parse the first found tag. // --> The only parse the first found tag.
return tag.replace('&', '&amp;'); return itunesCategory.replace('&', '&amp;');
} }
} }
@ -197,8 +221,9 @@ const getFormattedDescription = (claim) => {
function generateFeed(feedLink, channelClaim, claimsInChannel) { function generateFeed(feedLink, channelClaim, claimsInChannel) {
// --- Channel --- // --- Channel ---
let channelTitle = (channelClaim.value && channelClaim.value.title) || channelClaim.name;
const feed = new Rss({ const feed = new Rss({
title: ((channelClaim.value && channelClaim.value.title) || channelClaim.name) + ' on ' + SITE_NAME, title: channelTitle + ' on ' + SITE_NAME,
description: getFormattedDescription(channelClaim), description: getFormattedDescription(channelClaim),
feed_url: feedLink, feed_url: feedLink,
site_url: URL, site_url: URL,
@ -206,7 +231,7 @@ function generateFeed(feedLink, channelClaim, claimsInChannel) {
language: getLanguageValue(channelClaim), language: getLanguageValue(channelClaim),
custom_namespaces: { itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd' }, custom_namespaces: { itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd' },
custom_elements: [ custom_elements: [
{ 'itunes:author': channelClaim.name }, { 'itunes:author': channelTitle },
{ {
'itunes:category': [ 'itunes:category': [
{ {
@ -231,17 +256,20 @@ function generateFeed(feedLink, channelClaim, claimsInChannel) {
: ''; : '';
const description = thumbnailHtml + getFormattedDescription(c); 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({ feed.item({
title: title, title: title,
description: description, description: description,
url: `${URL}/${c.name}:${c.claim_id}`, url: url,
guid: undefined, // defaults to 'url' guid: undefined, // defaults to 'url'
author: undefined, // defaults feed author property author: undefined, // defaults feed author property
date: new Date(c.meta ? c.meta.creation_timestamp * 1000 : null), date: new Date(date),
enclosure: generateEnclosureForClaimContent(c), enclosure: generateEnclosureForClaimContent(c),
custom_elements: [ custom_elements: [
{ 'itunes:title': title }, { 'itunes:title': title },
{ 'itunes:author': channelClaim.name }, { 'itunes:author': channelTitle },
generateItunesImageElement(c), generateItunesImageElement(c),
generateItunesDurationElement(c), generateItunesDurationElement(c),
generateItunesExplicitElement(c), generateItunesExplicitElement(c),