add oEmbed Support for video claims (#376)

* Refactor html.js

* Fix Favicon

* Refactor rss.js

* Create oEmbed.js
This commit is contained in:
saltrafael 2021-11-29 23:27:56 -03:00 committed by GitHub
parent 7613d07c35
commit 34eaccdbee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 201 additions and 57 deletions

View file

@ -34,7 +34,6 @@ const config = {
TWITTER_ACCOUNT: process.env.TWITTER_ACCOUNT,
// LOGO
LOGO_TITLE: process.env.LOGO_TITLE,
FAVICON: process.env.FAVICON,
LOGO: process.env.LOGO,
LOGO_TEXT_LIGHT: process.env.LOGO_TEXT_LIGHT,
LOGO_TEXT_DARK: process.env.LOGO_TEXT_DARK,
@ -92,5 +91,6 @@ const config = {
config.URL_DEV = `http://localhost:${config.WEBPACK_WEB_PORT}`;
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
config.FAVICON = `/public/favicon-spaceman.png`;
module.exports = config;

View file

@ -1,39 +1,42 @@
const {
URL,
// DOMAIN,
SITE_TITLE,
SITE_CANONICAL_URL,
OG_HOMEPAGE_TITLE,
OG_TITLE_SUFFIX,
OG_IMAGE_URL,
SITE_DESCRIPTION,
SITE_NAME,
FAVICON,
LBRY_WEB_API,
OG_HOMEPAGE_TITLE,
OG_IMAGE_URL,
OG_TITLE_SUFFIX,
SITE_CANONICAL_URL,
SITE_DESCRIPTION,
SITE_NAME,
SITE_TITLE,
THUMBNAIL_CARDS_CDN_URL,
URL,
} = require('../../config.js');
const { lbryProxy: Lbry } = require('../lbry');
const { generateEmbedUrl, generateStreamUrl, generateDirectUrl } = require('../../ui/util/web');
const PAGES = require('../../ui/constants/pages');
const { CATEGORY_METADATA } = require('./category-metadata');
const { parseURI, normalizeURI } = require('./lbryURI');
const fs = require('fs');
const path = require('path');
const moment = require('moment');
const removeMd = require('remove-markdown');
const { generateEmbedUrl, generateStreamUrl, generateDirectUrl } = require('../../ui/util/web');
const { getJsBundleId } = require('../bundle-id.js');
const { lbryProxy: Lbry } = require('../lbry');
const { parseURI, normalizeClaimUrl } = require('./lbryURI');
const fs = require('fs');
const moment = require('moment');
const PAGES = require('../../ui/constants/pages');
const path = require('path');
const removeMd = require('remove-markdown');
const jsBundleId = getJsBundleId();
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const PROXY_URL = `${SDK_API_PATH}/proxy`;
Lbry.setDaemonConnectionString(PROXY_URL);
function getThumbnailCdnUrl(url) {
if (!THUMBNAIL_CARDS_CDN_URL || !url) {
return url;
}
const BEGIN_STR = '<!-- VARIABLE_HEAD_BEGIN -->';
const FINAL_STR = '<!-- VARIABLE_HEAD_END -->';
if (url && (url.includes('https://twitter-card') || url.includes('https://cards.odysee.com'))) {
function getThumbnailCdnUrl(url) {
if (
!THUMBNAIL_CARDS_CDN_URL ||
!url ||
(url && (url.includes('https://twitter-card') || url.includes('https://cards.odysee.com')))
) {
return url;
}
@ -44,16 +47,13 @@ function getThumbnailCdnUrl(url) {
}
function insertToHead(fullHtml, htmlToInsert) {
const beginStr = '<!-- VARIABLE_HEAD_BEGIN -->';
const finalStr = '<!-- VARIABLE_HEAD_END -->';
const beginIndex = fullHtml.indexOf(beginStr);
const finalIndex = fullHtml.indexOf(finalStr);
const beginIndex = fullHtml.indexOf(BEGIN_STR);
const finalIndex = fullHtml.indexOf(FINAL_STR);
if (beginIndex > -1 && finalIndex > -1 && finalIndex > beginIndex) {
return `${fullHtml.slice(0, beginIndex)}${
htmlToInsert || buildOgMetadata()
}<script src="/public/ui-${jsBundleId}.js" async></script>${fullHtml.slice(finalIndex + finalStr.length)}`;
}<script src="/public/ui-${jsBundleId}.js" async></script>${fullHtml.slice(finalIndex + FINAL_STR.length)}`;
}
}
@ -66,10 +66,6 @@ function truncateDescription(description, maxChars = 200) {
return chars.length > maxChars ? truncated + '...' : truncated;
}
function normalizeClaimUrl(url) {
return normalizeURI(url.replace(/:/g, '#'));
}
function escapeHtmlProperty(property) {
return property
? String(property)
@ -92,6 +88,7 @@ function getCategoryMeta(path) {
function buildOgMetadata(overrideOptions = {}) {
const { title, description, image, path } = overrideOptions;
const cleanDescription = removeMd(description || SITE_DESCRIPTION);
const head =
`<title>${SITE_TITLE}</title>\n` +
`<meta name="description" content="${cleanDescription}" />\n` +
@ -138,13 +135,12 @@ function addFavicon() {
}
function buildHead() {
const head =
'<!-- VARIABLE_HEAD_BEGIN -->' + addFavicon() + addPWA() + buildOgMetadata() + '<!-- VARIABLE_HEAD_END -->';
const head = BEGIN_STR + addFavicon() + addPWA() + buildOgMetadata() + FINAL_STR;
return head;
}
function buildBasicOgMetadata() {
const head = '<!-- VARIABLE_HEAD_BEGIN -->' + addFavicon() + buildOgMetadata() + '<!-- VARIABLE_HEAD_END -->';
const head = BEGIN_STR + addFavicon() + buildOgMetadata() + FINAL_STR;
return head;
}
@ -187,7 +183,7 @@ function buildClaimOgMetadata(uri, claim, overrideOptions = {}) {
getThumbnailCdnUrl(OG_IMAGE_URL) ||
`${URL}/public/v2-og.png`;
// Allow for ovverriding default claim based og metadata
// Allow for overriding default claim based og metadata
const title = overrideOptions.title || claimTitle;
const description = overrideOptions.description || claimDescription;
const cleanDescription = removeMd(description);
@ -218,6 +214,12 @@ function buildClaimOgMetadata(uri, claim, overrideOptions = {}) {
head += `<meta name="twitter:url" content="${URL}/${claim.name}:${claim.claim_id}"/>`;
head += `<meta property="fb:app_id" content="1673146449633983" />`;
head += `<link rel="canonical" content="${SITE_CANONICAL_URL || URL}/${claim.name}:${claim.claim_id}"/>`;
head += `<link rel="alternate" type="application/json+oembed" href="${URL}/$/oembed?url=${encodeURIComponent(
`${URL}/${claim.canonical_url}`
)}&format=json" title="${title}" />`;
head += `<link rel="alternate" type="text/xml+oembed" href="${URL}/$/oembed?url=${encodeURIComponent(
`${URL}/${claim.canonical_url}`
)}&format=xml" title="${title}" />`;
if (mediaType && (mediaType.startsWith('video/') || mediaType.startsWith('audio/'))) {
const videoUrl = generateEmbedUrl(claim.name, claim.claim_id);

View file

@ -330,10 +330,15 @@ function isURIEqual(uriA, uriB) {
}
}
function normalizeClaimUrl(url) {
return normalizeURI(url.replace(/:/g, '#'));
}
module.exports = {
parseURI,
buildURI,
normalizeURI,
normalizeClaimUrl,
isURIValid,
isURIEqual,
isNameValid,

130
web/src/oEmbed.js Normal file
View file

@ -0,0 +1,130 @@
const {
URL,
SITE_NAME,
LBRY_WEB_API,
THUMBNAIL_CARDS_CDN_URL,
THUMBNAIL_HEIGHT,
THUMBNAIL_WIDTH,
} = require('../../config.js');
const { generateEmbedUrl } = require('../../ui/util/web');
const { lbryProxy: Lbry } = require('../lbry');
const { normalizeURI } = require('./lbryURI');
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const proxyURL = `${SDK_API_PATH}/proxy`;
Lbry.setDaemonConnectionString(proxyURL);
// ****************************************************************************
// Fetch claim info
// ****************************************************************************
function getThumbnailCdnUrl(url) {
if (
!THUMBNAIL_CARDS_CDN_URL ||
!url ||
(url && (url.includes('https://twitter-card') || url.includes('https://cards.odysee.com')))
) {
return url;
}
if (url) {
const encodedURL = Buffer.from(url).toString('base64');
return `${THUMBNAIL_CARDS_CDN_URL}${encodedURL}.jpg`;
}
}
async function getClaim(requestUrl) {
const path = requestUrl.replace(URL, '').substring(1);
let uri;
let claim;
let error;
try {
uri = normalizeURI(path);
const response = await Lbry.resolve({ urls: [uri] });
if (response && response[uri] && !response[uri].error) {
claim = response[uri];
}
} catch {}
if (!claim) {
error = 'The URL is invalid or is not associated with any claim.';
} else {
const { value_type, value } = claim;
if (value_type !== 'stream' || value.stream_type !== 'video') {
error = 'The URL is not associated with a video claim.';
}
}
return { claim, error };
}
// ****************************************************************************
// Generate
// ****************************************************************************
function generateOEmbedData(claim) {
const { value, signing_channel: authorClaim } = claim;
const claimTitle = value.title;
const authorName = authorClaim ? authorClaim.value.title || authorClaim.name : 'Anonymous';
const authorUrlPath = authorClaim && authorClaim.canonical_url.replace('lbry://', '');
const authorUrl = authorClaim ? `${URL}/${authorUrlPath}` : null;
const thumbnailUrl = value && value.thumbnail && value.thumbnail.url && getThumbnailCdnUrl(value.thumbnail.url);
const videoUrl = generateEmbedUrl(claim.name, claim.claim_id);
const videoWidth = value.video && value.video.width;
const videoHeight = value.video && value.video.height;
return {
type: 'video',
version: '1.0',
title: claimTitle,
author_name: authorName,
author_url: authorUrl,
provider_name: SITE_NAME,
provider_url: URL,
thumbnail_url: thumbnailUrl,
thumbnail_width: THUMBNAIL_WIDTH,
thumbnail_height: THUMBNAIL_HEIGHT,
html: `<iframe id="lbry-iframe" width="560" height="315" src="${videoUrl}" allowfullscreen></iframe>`,
width: videoWidth,
height: videoHeight,
};
}
async function getOEmbed(ctx) {
const path = ctx.request.url;
const urlQuery = '?url=';
const formatQuery = '&format=';
const requestUrl = decodeURIComponent(
path.substring(
path.indexOf(urlQuery) + urlQuery.length,
path.indexOf('&') > path.indexOf(urlQuery) ? path.indexOf('&') : path.length
)
);
const requestFormat = path.substring(
path.indexOf(formatQuery) + formatQuery.length,
path.indexOf('&') > path.indexOf(formatQuery) ? path.indexOf('&') : path.length
);
const isXml = requestFormat === 'xml';
const { claim, error } = await getClaim(requestUrl);
if (error) return error;
const oEmbedData = generateOEmbedData(claim);
if (isXml) {
ctx.set('Content-Type', 'text/xml+oembed');
return oEmbedData.xml();
}
ctx.set('Content-Type', 'application/json+oembed');
return oEmbedData;
}
module.exports = { getOEmbed };

View file

@ -1,11 +1,13 @@
const { CUSTOM_HOMEPAGE } = require('../../config.js');
const { generateStreamUrl } = require('../../ui/util/web');
const { getHomepageJSON } = require('./getHomepageJSON');
const { getHtml } = require('./html');
const { getOEmbed } = require('./oEmbed');
const { getRss } = require('./rss');
const { getTempFile } = require('./tempfile');
const { getHomepageJSON } = require('./getHomepageJSON');
const { generateStreamUrl } = require('../../ui/util/web');
const fetch = require('node-fetch');
const Router = require('@koa/router');
const { CUSTOM_HOMEPAGE } = require('../../config.js');
// So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server
global.fetch = fetch;
@ -27,6 +29,11 @@ const rssMiddleware = async (ctx) => {
ctx.body = rss;
};
const oEmbedMiddleware = async (ctx) => {
const oEmbed = await getOEmbed(ctx);
ctx.body = oEmbed;
};
const tempfileMiddleware = async (ctx) => {
const temp = await getTempFile(ctx);
ctx.body = temp;
@ -76,6 +83,8 @@ router.get('/.well-known/:filename', tempfileMiddleware);
router.get(`/$/rss/:claimName/:claimId`, rssMiddleware);
router.get(`/$/rss/:claimName::claimId`, rssMiddleware);
router.get(`/$/oembed`, oEmbedMiddleware);
router.get('*', async (ctx) => {
const html = await getHtml(ctx);
ctx.body = html;

View file

@ -1,8 +1,8 @@
const { generateStreamUrl } = require('../../ui/util/web');
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
const { lbryProxy: Lbry } = require('../lbry');
const Rss = require('rss');
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
const Mime = require('mime-types');
const Rss = require('rss');
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const proxyURL = `${SDK_API_PATH}/proxy`;
@ -105,10 +105,11 @@ const generateEnclosureForClaimContent = (claim) => {
};
const getLanguageValue = (claim) => {
if (claim && claim.value && claim.value.languages && claim.value.languages.length > 0) {
return claim.value.languages[0];
}
return 'en';
const {
value: { languages },
} = claim;
return languages && languages.length > 0 ? languages[0] : 'en';
};
const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br />');
@ -127,12 +128,11 @@ const isEmailRoughlyValid = (email) => /^\S+@\S+$/.test(email);
*/
const generateItunesOwnerElement = (claim) => {
let email = 'no-reply@odysee.com';
let name = claim && (claim.value && claim.value.title ? claim.value.title : claim.name);
const { value } = claim;
const name = (value && value.title) || claim.name;
if (claim && claim.value) {
if (isEmailRoughlyValid(claim.value.email)) {
email = claim.value.email;
}
if (isEmailRoughlyValid(value.email)) {
email = value.email;
}
return {
@ -211,9 +211,7 @@ const generateItunesImageElement = (claim) => {
}
};
const getFormattedDescription = (claim) => {
return replaceLineFeeds((claim && claim.value && claim.value.description) || '');
};
const getFormattedDescription = (claim) => replaceLineFeeds(claim.value.description || '');
// ****************************************************************************
// Generate