RSS feed for channels
## Issue 3779 RSS feed for channels ## Initial implementation details - RSS only (not atom) - Grabs latest 10 entries (Beamer have concerns) ## Credit Referenced the community version mentioned in 3779
This commit is contained in:
parent
608a421ce5
commit
49046c9d25
7 changed files with 128 additions and 2 deletions
|
@ -53,6 +53,7 @@
|
||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"feed": "^4.2.2",
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"react-datetime-picker": "^3.2.1",
|
"react-datetime-picker": "^3.2.1",
|
||||||
"react-top-loading-bar": "^2.0.1",
|
"react-top-loading-bar": "^2.0.1",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { generateShareUrl } from 'util/url';
|
import { generateShareUrl, generateRssUrl } from 'util/url';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
|
@ -103,6 +103,7 @@ function ClaimMenuList(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
|
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
|
||||||
|
const rssUrl: string = generateRssUrl(SHARE_DOMAIN, uri);
|
||||||
const isCollectionClaim = claim && claim.value_type === 'collection';
|
const isCollectionClaim = claim && claim.value_type === 'collection';
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const isPlayable =
|
const isPlayable =
|
||||||
|
@ -182,6 +183,10 @@ function ClaimMenuList(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCopyRssLink() {
|
||||||
|
navigator.clipboard.writeText(rssUrl);
|
||||||
|
}
|
||||||
|
|
||||||
function handleCopyLink() {
|
function handleCopyLink() {
|
||||||
navigator.clipboard.writeText(shareUrl);
|
navigator.clipboard.writeText(shareUrl);
|
||||||
}
|
}
|
||||||
|
@ -350,7 +355,18 @@ function ClaimMenuList(props: Props) {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className="menu__separator" />
|
<hr className="menu__separator" />
|
||||||
|
|
||||||
|
{isChannelPage && (
|
||||||
|
<MenuItem className="comment__menu-option" onSelect={handleCopyRssLink}>
|
||||||
|
<div className="menu__link">
|
||||||
|
<Icon aria-hidden icon={ICONS.SHARE} />
|
||||||
|
{__('Copy RSS URL')}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.SHARE} />
|
<Icon aria-hidden icon={ICONS.SHARE} />
|
||||||
|
|
|
@ -147,3 +147,9 @@ export const generateShareUrl = (
|
||||||
const url = `${domain}/${lbryWebUrl}` + (urlParamsString === '' ? '' : `?${urlParamsString}`);
|
const url = `${domain}/${lbryWebUrl}` + (urlParamsString === '' ? '' : `?${urlParamsString}`);
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateRssUrl = (domain, lbryUrl) => {
|
||||||
|
const { channelName, channelClaimId } = parseURI(lbryUrl);
|
||||||
|
const url = `${domain}/$/rss/@${channelName}/${channelClaimId}`;
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ const pool = mysql.createPool({
|
||||||
});
|
});
|
||||||
|
|
||||||
function queryPool(sql, params) {
|
function queryPool(sql, params) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
pool.query(sql, params, (error, rows) => {
|
pool.query(sql, params, (error, rows) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log('error', error); // eslint-disable-line
|
console.log('error', error); // eslint-disable-line
|
||||||
|
@ -58,3 +58,21 @@ module.exports.getClaim = async function getClaim(claimName, claimId, channelNam
|
||||||
|
|
||||||
return queryPool(sql, params);
|
return queryPool(sql, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.getChannelClaim = async function getChannelClaim(channelClaimId) {
|
||||||
|
const params = [];
|
||||||
|
const select = ['claim_id', 'name', 'title', 'thumbnail_url', 'description'].join(', ');
|
||||||
|
|
||||||
|
const sql = `SELECT ${select} FROM claim WHERE claim_id = "${channelClaimId}"`;
|
||||||
|
return queryPool(sql, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.getClaimsFromChannel = async function getClaimsFromChannel(channelClaimId, count = 10) {
|
||||||
|
const params = [];
|
||||||
|
const select = ['claim_id', 'name', 'title', 'thumbnail_url', 'description', 'created_at'].join(', ');
|
||||||
|
const sort = 'ORDER BY created_at DESC';
|
||||||
|
const limit = `LIMIT ${count}`;
|
||||||
|
|
||||||
|
const sql = `SELECT ${select} FROM claim WHERE publisher_id = "${channelClaimId}" ${sort} ${limit}`;
|
||||||
|
return queryPool(sql, params);
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { getHtml } = require('./html');
|
const { getHtml } = require('./html');
|
||||||
|
const { getRss } = require('./rss');
|
||||||
const { generateStreamUrl } = require('../../ui/util/web');
|
const { generateStreamUrl } = require('../../ui/util/web');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const Router = require('@koa/router');
|
const Router = require('@koa/router');
|
||||||
|
@ -42,6 +43,12 @@ router.get(`/$/stream/:claimName/:claimId`, async (ctx) => {
|
||||||
ctx.redirect(streamUrl);
|
ctx.redirect(streamUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get(`/$/rss/:claimName/:claimId`, async (ctx) => {
|
||||||
|
const xml = await getRss(ctx);
|
||||||
|
ctx.set('Content-Type', 'application/rss+xml');
|
||||||
|
ctx.body = xml;
|
||||||
|
});
|
||||||
|
|
||||||
router.get('*', async (ctx) => {
|
router.get('*', async (ctx) => {
|
||||||
const html = await getHtml(ctx);
|
const html = await getHtml(ctx);
|
||||||
ctx.body = html;
|
ctx.body = html;
|
||||||
|
|
64
web/src/rss.js
Normal file
64
web/src/rss.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
const { URL, SITE_NAME } = require('../../config.js');
|
||||||
|
const { getChannelClaim, getClaimsFromChannel } = require('./chainquery');
|
||||||
|
const Feed = require('feed').Feed;
|
||||||
|
|
||||||
|
async function getChannelClaimFromChainquery(claimId) {
|
||||||
|
const rows = await getChannelClaim(claimId);
|
||||||
|
if (rows && rows.length) {
|
||||||
|
const claim = rows[0];
|
||||||
|
return claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFeed(channelClaim) {
|
||||||
|
const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
title: channelClaim.title + ' on ' + SITE_NAME,
|
||||||
|
description: channelClaim.description ? replaceLineFeeds(channelClaim.description) : '',
|
||||||
|
link: `${URL}/${channelClaim.name}:${channelClaim.claim_id}`,
|
||||||
|
favicon: URL + '/public/favicon.png',
|
||||||
|
generator: SITE_NAME + ' RSS Feed',
|
||||||
|
image: channelClaim.thumbnail_url,
|
||||||
|
author: {
|
||||||
|
name: channelClaim.name,
|
||||||
|
link: URL + '/' + channelClaim.name + ':' + channelClaim.claim_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const feed = new Feed(options);
|
||||||
|
|
||||||
|
const latestClaims = await getClaimsFromChannel(channelClaim.claim_id, 10);
|
||||||
|
|
||||||
|
latestClaims.forEach((c) => {
|
||||||
|
feed.addItem({
|
||||||
|
guid: c.claim_id,
|
||||||
|
id: c.claim_id,
|
||||||
|
title: c.title,
|
||||||
|
description: c.description ? replaceLineFeeds(c.description) : '',
|
||||||
|
image: c.thumbnail_url,
|
||||||
|
link: URL + '/' + c.name + ':' + c.claim_id,
|
||||||
|
date: new Date(c.created_at),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return feed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRss(ctx) {
|
||||||
|
if (!ctx.params.claimName || !ctx.params.claimId) {
|
||||||
|
return 'Invalid URL';
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelClaim = await getChannelClaimFromChainquery(ctx.params.claimId);
|
||||||
|
if (channelClaim) {
|
||||||
|
const feed = await getFeed(channelClaim);
|
||||||
|
return feed.rss2();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Invalid channel';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getRss };
|
14
yarn.lock
14
yarn.lock
|
@ -5220,6 +5220,13 @@ fd-slicer@~1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pend "~1.2.0"
|
pend "~1.2.0"
|
||||||
|
|
||||||
|
feed@^4.2.2:
|
||||||
|
version "4.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
|
||||||
|
integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==
|
||||||
|
dependencies:
|
||||||
|
xml-js "^1.6.11"
|
||||||
|
|
||||||
figgy-pudding@^3.5.1:
|
figgy-pudding@^3.5.1:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
||||||
|
@ -12512,6 +12519,13 @@ xdg-basedir@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||||
|
|
||||||
|
xml-js@^1.6.11:
|
||||||
|
version "1.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
|
||||||
|
integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
|
||||||
|
dependencies:
|
||||||
|
sax "^1.2.4"
|
||||||
|
|
||||||
xmlbuilder@^10.0.0:
|
xmlbuilder@^10.0.0:
|
||||||
version "10.1.1"
|
version "10.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"
|
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"
|
||||||
|
|
Loading…
Reference in a new issue