Fix unfurler language support
This commit is contained in:
parent
06f232fdd8
commit
94d1aeb287
@ -76,7 +76,7 @@ export class OpenGraphService {
|
|||||||
this.metaService.updateTag({ property: 'og:preview:loading', content: 'loading'});
|
this.metaService.updateTag({ property: 'og:preview:loading', content: 'loading'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// signal that an event has resolved
|
// mark an event as resolved
|
||||||
// if all registered events have resolved, signal we are ready for a screenshot
|
// if all registered events have resolved, signal we are ready for a screenshot
|
||||||
waitOver(event) {
|
waitOver(event) {
|
||||||
if (this.previewLoadingEvents[event]) {
|
if (this.previewLoadingEvents[event]) {
|
||||||
@ -100,9 +100,11 @@ export class OpenGraphService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadPage(path) {
|
loadPage(path) {
|
||||||
this.resetLoading();
|
if (path !== this.router.url) {
|
||||||
this.ngZone.run(() => {
|
this.resetLoading();
|
||||||
this.router.navigateByUrl(path);
|
this.ngZone.run(() => {
|
||||||
})
|
this.router.navigateByUrl(path);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,13 @@ import { timeoutExecute } from 'puppeteer-cluster/dist/util';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
const mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : '');
|
const mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : '');
|
||||||
|
|
||||||
const BROWSER_TIMEOUT = 5000;
|
const BROWSER_TIMEOUT = 8000;
|
||||||
// maximum lifetime of a single page session
|
// maximum lifetime of a single page session
|
||||||
const maxAgeMs = (config.PUPPETEER.MAX_PAGE_AGE || (24 * 60 * 60)) * 1000;
|
const maxAgeMs = (config.PUPPETEER.MAX_PAGE_AGE || (24 * 60 * 60)) * 1000;
|
||||||
|
|
||||||
interface repairablePage extends puppeteer.Page {
|
interface repairablePage extends puppeteer.Page {
|
||||||
repairRequested?: boolean;
|
repairRequested?: boolean;
|
||||||
|
language?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ReusablePage extends ConcurrencyImplementation {
|
export default class ReusablePage extends ConcurrencyImplementation {
|
||||||
@ -68,6 +69,7 @@ export default class ReusablePage extends ConcurrencyImplementation {
|
|||||||
protected async createResources(): Promise<ResourceData> {
|
protected async createResources(): Promise<ResourceData> {
|
||||||
if (!this.currentPage) {
|
if (!this.currentPage) {
|
||||||
this.currentPage = await (this.browser as puppeteer.Browser).newPage();
|
this.currentPage = await (this.browser as puppeteer.Browser).newPage();
|
||||||
|
this.currentPage.language = null;
|
||||||
this.pageCreatedAt = Date.now();
|
this.pageCreatedAt = Date.now();
|
||||||
const defaultUrl = mempoolHost + '/preview/block/1';
|
const defaultUrl = mempoolHost + '/preview/block/1';
|
||||||
this.currentPage.on('pageerror', (err) => {
|
this.currentPage.on('pageerror', (err) => {
|
||||||
@ -85,10 +87,6 @@ export default class ReusablePage extends ConcurrencyImplementation {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
jobInstance: async () => {
|
jobInstance: async () => {
|
||||||
if (this.repairRequested || this.currentPage?.repairRequested) {
|
|
||||||
await this.repair();
|
|
||||||
}
|
|
||||||
|
|
||||||
await timeoutExecute(BROWSER_TIMEOUT, (async () => {
|
await timeoutExecute(BROWSER_TIMEOUT, (async () => {
|
||||||
resources = await this.createResources();
|
resources = await this.createResources();
|
||||||
})());
|
})());
|
||||||
|
@ -4,6 +4,7 @@ import * as http from 'http';
|
|||||||
import config from './config';
|
import config from './config';
|
||||||
import { Cluster } from 'puppeteer-cluster';
|
import { Cluster } from 'puppeteer-cluster';
|
||||||
import ReusablePage from './concurrency/ReusablePage';
|
import ReusablePage from './concurrency/ReusablePage';
|
||||||
|
import { parseLanguageUrl } from './language/lang';
|
||||||
const puppeteerConfig = require('../puppeteer.config.json');
|
const puppeteerConfig = require('../puppeteer.config.json');
|
||||||
|
|
||||||
if (config.PUPPETEER.EXEC_PATH) {
|
if (config.PUPPETEER.EXEC_PATH) {
|
||||||
@ -46,6 +47,8 @@ class Server {
|
|||||||
this.server.listen(config.SERVER.HTTP_PORT, () => {
|
this.server.listen(config.SERVER.HTTP_PORT, () => {
|
||||||
console.log(`Mempool Unfurl Server is running on port ${config.SERVER.HTTP_PORT}`);
|
console.log(`Mempool Unfurl Server is running on port ${config.SERVER.HTTP_PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initClusterPages();
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopServer() {
|
async stopServer() {
|
||||||
@ -63,9 +66,24 @@ class Server {
|
|||||||
this.app.get('*', (req, res) => { return this.renderHTML(req, res) })
|
this.app.get('*', (req, res) => { return this.renderHTML(req, res) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initClusterPages() {
|
||||||
|
for (let i = 0; i < config.PUPPETEER.CLUSTER_SIZE; i++) {
|
||||||
|
this.cluster?.execute({ action: 'init' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async clusterTask({ page, data: { url, path, action } }) {
|
async clusterTask({ page, data: { url, path, action } }) {
|
||||||
|
if (action === 'init') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (action === 'screenshot' || action === 'html') {
|
const urlParts = parseLanguageUrl(path);
|
||||||
|
if (page.language !== urlParts.lang) {
|
||||||
|
// switch language
|
||||||
|
page.language = urlParts.lang;
|
||||||
|
const localizedUrl = urlParts.lang ? `${this.mempoolHost}/${urlParts.lang}${urlParts.path}` : `${this.mempoolHost}${urlParts.path}` ;
|
||||||
|
await page.goto(localizedUrl, { waitUntil: "load" });
|
||||||
|
} else {
|
||||||
const loaded = await page.evaluate(async (path) => {
|
const loaded = await page.evaluate(async (path) => {
|
||||||
if (window['ogService']) {
|
if (window['ogService']) {
|
||||||
window['ogService'].loadPage(path);
|
window['ogService'].loadPage(path);
|
||||||
@ -73,26 +91,21 @@ class Server {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, path)
|
}, urlParts.path);
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
throw new Error('failed to access open graph service');
|
throw new Error('failed to access open graph service');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'screenshot') {
|
if (action === 'screenshot') {
|
||||||
const waitForReady = await page.$('meta[property="og:preview:loading"]');
|
const waitForReady = await page.$('meta[property="og:preview:loading"]');
|
||||||
const alreadyReady = await page.$('meta[property="og:preview:ready"]');
|
if (waitForReady != null) {
|
||||||
if (waitForReady != null && alreadyReady == null) {
|
await page.waitForSelector('meta[property="og:preview:ready"]', { timeout: 3000 });
|
||||||
await page.waitForSelector('meta[property="og:preview:ready"]', { timeout: 8000 });
|
|
||||||
}
|
|
||||||
return page.screenshot();
|
|
||||||
} else if (action === 'html') {
|
|
||||||
const alreadyReady = await page.$('meta[property="og:meta:ready"]');
|
|
||||||
if (alreadyReady == null) {
|
|
||||||
await page.waitForSelector('meta[property="og:meta:ready"]', { timeout: 8000 });
|
|
||||||
}
|
|
||||||
return page.content();
|
|
||||||
}
|
}
|
||||||
|
return page.screenshot();
|
||||||
|
} else if (action === 'html') {
|
||||||
|
await page.waitForSelector('meta[property="og:meta:ready"]', { timeout: 3000 });
|
||||||
|
return page.content();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`failed to render page for ${action}`, e instanceof Error ? e.message : e);
|
console.log(`failed to render page for ${action}`, e instanceof Error ? e.message : e);
|
||||||
@ -102,13 +115,13 @@ class Server {
|
|||||||
|
|
||||||
async renderPreview(req, res) {
|
async renderPreview(req, res) {
|
||||||
try {
|
try {
|
||||||
// strip default language code for compatibility
|
const path = req.params[0]
|
||||||
const path = req.params[0].replace('/en/', '/');
|
|
||||||
const img = await this.cluster?.execute({ url: this.mempoolHost + path, path: path, action: 'screenshot' });
|
const img = await this.cluster?.execute({ url: this.mempoolHost + path, path: path, action: 'screenshot' });
|
||||||
|
|
||||||
if (!img) {
|
if (!img) {
|
||||||
throw new Error('failed to render preview image');
|
throw new Error('failed to render preview image');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.contentType('image/png');
|
res.contentType('image/png');
|
||||||
res.send(img);
|
res.send(img);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -127,10 +140,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// strip default language code for compatibility
|
let html = await this.cluster?.execute({ url: this.mempoolHost + path, path: path, action: 'html' });
|
||||||
const path = req.params[0].replace('/en/', '/');
|
|
||||||
|
|
||||||
let html = await this.cluster?.execute({ url: this.mempoolHost + req.params[0], path: req.params[0], action: 'html' });
|
|
||||||
if (!html) {
|
if (!html) {
|
||||||
throw new Error('failed to render preview image');
|
throw new Error('failed to render preview image');
|
||||||
}
|
}
|
||||||
|
79
unfurler/src/language/lang.ts
Normal file
79
unfurler/src/language/lang.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
export interface Language {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageList: Language[] = [
|
||||||
|
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||||
|
{ code: 'bg', name: 'Български' }, // Bulgarian
|
||||||
|
{ code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||||
|
{ code: 'ca', name: 'Català' }, // Catalan
|
||||||
|
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||||
|
{ code: 'da', name: 'Dansk' }, // Danish
|
||||||
|
{ code: 'de', name: 'Deutsch' }, // German
|
||||||
|
{ code: 'et', name: 'Eesti' }, // Estonian
|
||||||
|
{ code: 'el', name: 'Ελληνικά' }, // Greek
|
||||||
|
{ code: 'en', name: 'English' }, // English
|
||||||
|
{ code: 'es', name: 'Español' }, // Spanish
|
||||||
|
{ code: 'eo', name: 'Esperanto' }, // Esperanto
|
||||||
|
{ code: 'eu', name: 'Euskara' }, // Basque
|
||||||
|
{ code: 'fa', name: 'فارسی' }, // Persian
|
||||||
|
{ code: 'fr', name: 'Français' }, // French
|
||||||
|
{ code: 'gl', name: 'Galego' }, // Galician
|
||||||
|
{ code: 'ko', name: '한국어' }, // Korean
|
||||||
|
{ code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||||
|
{ code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||||
|
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||||
|
{ code: 'it', name: 'Italiano' }, // Italian
|
||||||
|
{ code: 'he', name: 'עברית' }, // Hebrew
|
||||||
|
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||||
|
{ code: 'lv', name: 'Latviešu' }, // Latvian
|
||||||
|
{ code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
||||||
|
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
||||||
|
{ code: 'mk', name: 'Македонски' }, // Macedonian
|
||||||
|
{ code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
||||||
|
{ code: 'nl', name: 'Nederlands' }, // Dutch
|
||||||
|
{ code: 'ja', name: '日本語' }, // Japanese
|
||||||
|
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
|
||||||
|
{ code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
|
||||||
|
{ code: 'pl', name: 'Polski' }, // Polish
|
||||||
|
{ code: 'pt', name: 'Português' }, // Portuguese
|
||||||
|
{ code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
||||||
|
{ code: 'ro', name: 'Română' }, // Romanian
|
||||||
|
{ code: 'ru', name: 'Русский' }, // Russian
|
||||||
|
{ code: 'sk', name: 'Slovenčina' }, // Slovak
|
||||||
|
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
||||||
|
{ code: 'sr', name: 'Српски / srpski' }, // Serbian
|
||||||
|
{ code: 'sh', name: 'Srpskohrvatski / српскохрватски' },// Serbo-Croatian
|
||||||
|
{ code: 'fi', name: 'Suomi' }, // Finnish
|
||||||
|
{ code: 'sv', name: 'Svenska' }, // Swedish
|
||||||
|
{ code: 'th', name: 'ไทย' }, // Thai
|
||||||
|
{ code: 'tr', name: 'Türkçe' }, // Turkish
|
||||||
|
{ code: 'uk', name: 'Українська' }, // Ukrainian
|
||||||
|
{ code: 'vi', name: 'Tiếng Việt' }, // Vietnamese
|
||||||
|
{ code: 'zh', name: '中文' }, // Chinese
|
||||||
|
];
|
||||||
|
|
||||||
|
const languageDict = {};
|
||||||
|
languageList.forEach(lang => {
|
||||||
|
languageDict[lang.code] = lang
|
||||||
|
});
|
||||||
|
export const languages = languageDict;
|
||||||
|
|
||||||
|
// expects path to start with a leading '/'
|
||||||
|
export function parseLanguageUrl(path) {
|
||||||
|
const parts = path.split('/');
|
||||||
|
let lang;
|
||||||
|
let rest;
|
||||||
|
if (languages[parts[1]]) {
|
||||||
|
lang = parts[1];
|
||||||
|
rest = '/' + parts.slice(2).join('/');
|
||||||
|
} else {
|
||||||
|
lang = null;
|
||||||
|
rest = path;
|
||||||
|
}
|
||||||
|
if (lang === 'en') {
|
||||||
|
lang = null;
|
||||||
|
}
|
||||||
|
return { lang, path: rest };
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user