|
@@ -5,8 +5,7 @@ import axios from 'axios';
|
|
|
import * as CallableInstance from 'callable-instance';
|
|
|
import { XmlEntities } from 'html-entities';
|
|
|
import { PNG } from 'pngjs';
|
|
|
-import * as puppeteer from 'puppeteer';
|
|
|
-import { Browser } from 'puppeteer';
|
|
|
+import * as puppeteer from 'playwright';
|
|
|
import * as sharp from 'sharp';
|
|
|
|
|
|
import { getLogger } from './loggers';
|
|
@@ -31,23 +30,30 @@ const logger = getLogger('webshot');
|
|
|
|
|
|
class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Promise<void>> {
|
|
|
|
|
|
- private browser: Browser;
|
|
|
+ private browser: puppeteer.Browser;
|
|
|
private mode: number;
|
|
|
+ private wsUrl: string;
|
|
|
|
|
|
- constructor(mode: number, onready?: (...args) => void) {
|
|
|
+ constructor(wsUrl: string, mode: number, onready?: (...args) => void) {
|
|
|
super('webshot');
|
|
|
// tslint:disable-next-line: no-conditional-assignment
|
|
|
// eslint-disable-next-line no-cond-assign
|
|
|
if (this.mode = mode) {
|
|
|
onready();
|
|
|
} else {
|
|
|
+ this.wsUrl = wsUrl;
|
|
|
this.connect(onready);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // use local Chromium
|
|
|
- private connect = (onready: (...args) => void): Promise<void> =>
|
|
|
- puppeteer.connect({browserURL: 'http://127.0.0.1:9222'})
|
|
|
+ private connect = (onready?: (...args) => void): Promise<void> =>
|
|
|
+ axios.get<{[key in 'chromium' | 'firefox' | 'webkit']?: string}>(this.wsUrl)
|
|
|
+ .then(res => {
|
|
|
+ logger.info(`received websocket endpoint: ${JSON.stringify(res.data)}`);
|
|
|
+ const browserType = Object.keys(res.data)[0] as keyof typeof res.data;
|
|
|
+ return (puppeteer[browserType] as puppeteer.BrowserType<puppeteer.Browser>)
|
|
|
+ .connect({wsEndpoint: res.data[browserType]});
|
|
|
+ })
|
|
|
.then(browser => this.browser = browser)
|
|
|
.then(() => {
|
|
|
logger.info('launched puppeteer browser');
|
|
@@ -75,24 +81,29 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
const width = 720;
|
|
|
const zoomFactor = 2;
|
|
|
logger.info(`shooting ${width}*${height} webshot for ${url}`);
|
|
|
- this.browser.newPage()
|
|
|
+ this.browser.newPage({
|
|
|
+ bypassCSP: true,
|
|
|
+ deviceScaleFactor: zoomFactor,
|
|
|
+ locale: 'ja-JP',
|
|
|
+ timezoneId: 'Asia/Tokyo',
|
|
|
+ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
|
|
|
+ })
|
|
|
.then(page => {
|
|
|
const startTime = new Date().getTime();
|
|
|
const getTimerTime = () => new Date().getTime() - startTime;
|
|
|
const getTimeout = () => Math.max(500, webshotDelay - getTimerTime());
|
|
|
- page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
|
|
|
- .then(() => page.setViewport({
|
|
|
- width: width / zoomFactor,
|
|
|
- height: height / zoomFactor,
|
|
|
- isMobile: true,
|
|
|
- deviceScaleFactor: zoomFactor,
|
|
|
- }))
|
|
|
- .then(() => page.setBypassCSP(true))
|
|
|
+ page.setViewportSize({
|
|
|
+ width: width / zoomFactor,
|
|
|
+ height: height / zoomFactor,
|
|
|
+ })
|
|
|
.then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
|
|
|
// hide header, "more options" button, like and retweet count
|
|
|
.then(() => page.addStyleTag({
|
|
|
content: 'header{display:none!important}path[d=\'M20.207 7.043a1 1 0 0 0-1.414 0L12 13.836 5.207 7.043a1 1 0 0 0-1.414 1.414l7.5 7.5a.996.996 0 0 0 1.414 0l7.5-7.5a1 1 0 0 0 0-1.414z\'],div[role=\'button\']{display: none;}',
|
|
|
}))
|
|
|
+ .then(() => page.addStyleTag({
|
|
|
+ content: '*{font-family:-apple-system,".Helvetica Neue DeskInterface",Hiragino Sans,Hiragino Sans GB,sans-serif!important}',
|
|
|
+ }))
|
|
|
// remove listeners
|
|
|
.then(() => page.evaluate(() => {
|
|
|
const poll = setInterval(() => {
|
|
@@ -153,7 +164,7 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
const idx = (x: number, y: number) => (this.width * y + x) << 2;
|
|
|
let boundary: number = null;
|
|
|
let x = zoomFactor * 2;
|
|
|
- for (let y = 0; y < this.height; y++) {
|
|
|
+ for (let y = 0; y < this.height; y += zoomFactor) {
|
|
|
if (
|
|
|
this.data[idx(x, y)] !== 255 &&
|
|
|
this.data[idx(x, y)] === this.data[idx(x + zoomFactor * 10, y)]
|
|
@@ -176,7 +187,7 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
x = Math.floor(16 * zoomFactor);
|
|
|
let flag = false;
|
|
|
let cnt = 0;
|
|
|
- for (let y = this.height - 1; y >= 0; y--) {
|
|
|
+ for (let y = this.height - 1 - zoomFactor; y >= 0; y -= zoomFactor) {
|
|
|
if ((this.data[idx(x, y)] === 255) === flag) {
|
|
|
cnt++;
|
|
|
flag = !flag;
|
|
@@ -190,16 +201,16 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
// if there are a "retweet" count and "like" count row, this will be the line above it
|
|
|
if (cnt === 4) {
|
|
|
const b = y + 1;
|
|
|
- if (this.height - boundary - (boundary - b) <= 1) {
|
|
|
+ if (Math.abs(this.height - boundary - (boundary - b)) <= 3 * zoomFactor) {
|
|
|
boundary = b;
|
|
|
- // }
|
|
|
- // }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // // if "retweet" count and "like" count are two rows, this will be the line above the first
|
|
|
- // if (cnt === 6) {
|
|
|
- // const c = y + 1;
|
|
|
- // if (this.height - boundary - 2 * (boundary - c) <= 2) {
|
|
|
- // boundary = c;
|
|
|
+ // if "retweet" count and "like" count are two rows, this will be the line above the first
|
|
|
+ if (cnt === 6) {
|
|
|
+ const c = y + 1;
|
|
|
+ if (Math.abs(this.height - boundary - 2 * (boundary - c)) <= 3 * zoomFactor) {
|
|
|
+ boundary = c;
|
|
|
break;
|
|
|
}
|
|
|
}
|