add oEmbed Support for video claims (#376)
* Refactor html.js * Fix Favicon * Refactor rss.js * Create oEmbed.js
This commit is contained in:
parent
7613d07c35
commit
34eaccdbee
6 changed files with 201 additions and 57 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
130
web/src/oEmbed.js
Normal 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 };
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue