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

@ -6,11 +6,11 @@ const config = {
WEBPACK_WEB_PORT: process.env.WEBPACK_WEB_PORT, WEBPACK_WEB_PORT: process.env.WEBPACK_WEB_PORT,
WEBPACK_ELECTRON_PORT: process.env.WEBPACK_ELECTRON_PORT, WEBPACK_ELECTRON_PORT: process.env.WEBPACK_ELECTRON_PORT,
WEB_SERVER_PORT: process.env.WEB_SERVER_PORT, WEB_SERVER_PORT: process.env.WEB_SERVER_PORT,
LBRY_WEB_API: process.env.LBRY_WEB_API, //api.na-backend.odysee.com', LBRY_WEB_API: process.env.LBRY_WEB_API, // api.na-backend.odysee.com',
LBRY_WEB_PUBLISH_API: process.env.LBRY_WEB_PUBLISH_API, LBRY_WEB_PUBLISH_API: process.env.LBRY_WEB_PUBLISH_API,
LBRY_WEB_PUBLISH_API_V2: process.env.LBRY_WEB_PUBLISH_API_V2, LBRY_WEB_PUBLISH_API_V2: process.env.LBRY_WEB_PUBLISH_API_V2,
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com', LBRY_API_URL: process.env.LBRY_API_URL, // api.lbry.com',
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //cdn.lbryplayer.xyz', LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, // cdn.lbryplayer.xyz',
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API, LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API, SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
SEARCH_SERVER_API_ALT: process.env.SEARCH_SERVER_API_ALT, SEARCH_SERVER_API_ALT: process.env.SEARCH_SERVER_API_ALT,
@ -34,7 +34,6 @@ const config = {
TWITTER_ACCOUNT: process.env.TWITTER_ACCOUNT, TWITTER_ACCOUNT: process.env.TWITTER_ACCOUNT,
// LOGO // LOGO
LOGO_TITLE: process.env.LOGO_TITLE, LOGO_TITLE: process.env.LOGO_TITLE,
FAVICON: process.env.FAVICON,
LOGO: process.env.LOGO, LOGO: process.env.LOGO,
LOGO_TEXT_LIGHT: process.env.LOGO_TEXT_LIGHT, LOGO_TEXT_LIGHT: process.env.LOGO_TEXT_LIGHT,
LOGO_TEXT_DARK: process.env.LOGO_TEXT_DARK, 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_DEV = `http://localhost:${config.WEBPACK_WEB_PORT}`;
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`; config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
config.FAVICON = `/public/favicon-spaceman.png`;
module.exports = config; module.exports = config;

View file

@ -1,39 +1,42 @@
const { const {
URL,
// DOMAIN,
SITE_TITLE,
SITE_CANONICAL_URL,
OG_HOMEPAGE_TITLE,
OG_TITLE_SUFFIX,
OG_IMAGE_URL,
SITE_DESCRIPTION,
SITE_NAME,
FAVICON, FAVICON,
LBRY_WEB_API, 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, THUMBNAIL_CARDS_CDN_URL,
URL,
} = require('../../config.js'); } = 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 { CATEGORY_METADATA } = require('./category-metadata');
const { parseURI, normalizeURI } = require('./lbryURI'); const { generateEmbedUrl, generateStreamUrl, generateDirectUrl } = require('../../ui/util/web');
const fs = require('fs');
const path = require('path');
const moment = require('moment');
const removeMd = require('remove-markdown');
const { getJsBundleId } = require('../bundle-id.js'); 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 jsBundleId = getJsBundleId();
const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`; const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const PROXY_URL = `${SDK_API_PATH}/proxy`; const PROXY_URL = `${SDK_API_PATH}/proxy`;
Lbry.setDaemonConnectionString(PROXY_URL); Lbry.setDaemonConnectionString(PROXY_URL);
function getThumbnailCdnUrl(url) { const BEGIN_STR = '<!-- VARIABLE_HEAD_BEGIN -->';
if (!THUMBNAIL_CARDS_CDN_URL || !url) { const FINAL_STR = '<!-- VARIABLE_HEAD_END -->';
return url;
}
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; return url;
} }
@ -44,16 +47,13 @@ function getThumbnailCdnUrl(url) {
} }
function insertToHead(fullHtml, htmlToInsert) { function insertToHead(fullHtml, htmlToInsert) {
const beginStr = '<!-- VARIABLE_HEAD_BEGIN -->'; const beginIndex = fullHtml.indexOf(BEGIN_STR);
const finalStr = '<!-- VARIABLE_HEAD_END -->'; const finalIndex = fullHtml.indexOf(FINAL_STR);
const beginIndex = fullHtml.indexOf(beginStr);
const finalIndex = fullHtml.indexOf(finalStr);
if (beginIndex > -1 && finalIndex > -1 && finalIndex > beginIndex) { if (beginIndex > -1 && finalIndex > -1 && finalIndex > beginIndex) {
return `${fullHtml.slice(0, beginIndex)}${ return `${fullHtml.slice(0, beginIndex)}${
htmlToInsert || buildOgMetadata() 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; return chars.length > maxChars ? truncated + '...' : truncated;
} }
function normalizeClaimUrl(url) {
return normalizeURI(url.replace(/:/g, '#'));
}
function escapeHtmlProperty(property) { function escapeHtmlProperty(property) {
return property return property
? String(property) ? String(property)
@ -92,6 +88,7 @@ function getCategoryMeta(path) {
function buildOgMetadata(overrideOptions = {}) { function buildOgMetadata(overrideOptions = {}) {
const { title, description, image, path } = overrideOptions; const { title, description, image, path } = overrideOptions;
const cleanDescription = removeMd(description || SITE_DESCRIPTION); const cleanDescription = removeMd(description || SITE_DESCRIPTION);
const head = const head =
`<title>${SITE_TITLE}</title>\n` + `<title>${SITE_TITLE}</title>\n` +
`<meta name="description" content="${cleanDescription}" />\n` + `<meta name="description" content="${cleanDescription}" />\n` +
@ -138,13 +135,12 @@ function addFavicon() {
} }
function buildHead() { function buildHead() {
const head = const head = BEGIN_STR + addFavicon() + addPWA() + buildOgMetadata() + FINAL_STR;
'<!-- VARIABLE_HEAD_BEGIN -->' + addFavicon() + addPWA() + buildOgMetadata() + '<!-- VARIABLE_HEAD_END -->';
return head; return head;
} }
function buildBasicOgMetadata() { function buildBasicOgMetadata() {
const head = '<!-- VARIABLE_HEAD_BEGIN -->' + addFavicon() + buildOgMetadata() + '<!-- VARIABLE_HEAD_END -->'; const head = BEGIN_STR + addFavicon() + buildOgMetadata() + FINAL_STR;
return head; return head;
} }
@ -187,7 +183,7 @@ function buildClaimOgMetadata(uri, claim, overrideOptions = {}) {
getThumbnailCdnUrl(OG_IMAGE_URL) || getThumbnailCdnUrl(OG_IMAGE_URL) ||
`${URL}/public/v2-og.png`; `${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 title = overrideOptions.title || claimTitle;
const description = overrideOptions.description || claimDescription; const description = overrideOptions.description || claimDescription;
const cleanDescription = removeMd(description); 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 name="twitter:url" content="${URL}/${claim.name}:${claim.claim_id}"/>`;
head += `<meta property="fb:app_id" content="1673146449633983" />`; 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="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/'))) { if (mediaType && (mediaType.startsWith('video/') || mediaType.startsWith('audio/'))) {
const videoUrl = generateEmbedUrl(claim.name, claim.claim_id); 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 = { module.exports = {
parseURI, parseURI,
buildURI, buildURI,
normalizeURI, normalizeURI,
normalizeClaimUrl,
isURIValid, isURIValid,
isURIEqual, isURIEqual,
isNameValid, 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 { getHtml } = require('./html');
const { getOEmbed } = require('./oEmbed');
const { getRss } = require('./rss'); const { getRss } = require('./rss');
const { getTempFile } = require('./tempfile'); const { getTempFile } = require('./tempfile');
const { getHomepageJSON } = require('./getHomepageJSON');
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');
const { CUSTOM_HOMEPAGE } = require('../../config.js');
// So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server // So any code from 'lbry-redux'/'lbryinc' that uses `fetch` can be run on the server
global.fetch = fetch; global.fetch = fetch;
@ -27,6 +29,11 @@ const rssMiddleware = async (ctx) => {
ctx.body = rss; ctx.body = rss;
}; };
const oEmbedMiddleware = async (ctx) => {
const oEmbed = await getOEmbed(ctx);
ctx.body = oEmbed;
};
const tempfileMiddleware = async (ctx) => { const tempfileMiddleware = async (ctx) => {
const temp = await getTempFile(ctx); const temp = await getTempFile(ctx);
ctx.body = temp; 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(`/$/rss/:claimName::claimId`, rssMiddleware); router.get(`/$/rss/:claimName::claimId`, rssMiddleware);
router.get(`/$/oembed`, oEmbedMiddleware);
router.get('*', async (ctx) => { router.get('*', async (ctx) => {
const html = await getHtml(ctx); const html = await getHtml(ctx);
ctx.body = html; ctx.body = html;

View file

@ -1,8 +1,8 @@
const { generateStreamUrl } = require('../../ui/util/web'); const { generateStreamUrl } = require('../../ui/util/web');
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
const { lbryProxy: Lbry } = require('../lbry'); 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 Mime = require('mime-types');
const Rss = require('rss');
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`;
@ -105,10 +105,11 @@ const generateEnclosureForClaimContent = (claim) => {
}; };
const getLanguageValue = (claim) => { const getLanguageValue = (claim) => {
if (claim && claim.value && claim.value.languages && claim.value.languages.length > 0) { const {
return claim.value.languages[0]; value: { languages },
} } = claim;
return 'en';
return languages && languages.length > 0 ? languages[0] : 'en';
}; };
const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br />'); 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) => { const generateItunesOwnerElement = (claim) => {
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); const { value } = claim;
const name = (value && value.title) || claim.name;
if (claim && claim.value) { if (isEmailRoughlyValid(value.email)) {
if (isEmailRoughlyValid(claim.value.email)) { email = value.email;
email = claim.value.email;
}
} }
return { return {
@ -211,9 +211,7 @@ const generateItunesImageElement = (claim) => {
} }
}; };
const getFormattedDescription = (claim) => { const getFormattedDescription = (claim) => replaceLineFeeds(claim.value.description || '');
return replaceLineFeeds((claim && claim.value && claim.value.description) || '');
};
// **************************************************************************** // ****************************************************************************
// Generate // Generate