Modernize gtranslate scraper
This commit is contained in:
parent
72568aa9b5
commit
67770a9c70
5 changed files with 139 additions and 94 deletions
|
@ -8,7 +8,7 @@ let con = new BackgroundMessenger();
|
|||
|
||||
const scraper = new GTranslateScraper();
|
||||
con.addMessageListener("translate", async (toTranslate, sender) =>
|
||||
scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id)
|
||||
scraper.translate(toTranslate, await getCurrentLanguages(), sender!.tab?.id)
|
||||
);
|
||||
|
||||
con.addMessageListener("getLanguages", () => scraper.languages);
|
||||
|
@ -62,8 +62,8 @@ const injectScript = async (tabID: number, enabled?: boolean) => {
|
|||
|
||||
con.addMessageListener(
|
||||
"setEnabled",
|
||||
async (v: boolean, s: Runtime.MessageSender) => {
|
||||
const tab = await getTabID(s);
|
||||
async (v: boolean, s?: Runtime.MessageSender) => {
|
||||
const tab = await getTabID(s!);
|
||||
if ((await isEnabledSession(tab)) == v) return;
|
||||
|
||||
injectScript(tab);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { browser } from "webextension-polyfill-ts";
|
||||
import { ContentScriptMessenger } from "./gtranslate_commands";
|
||||
|
||||
export enum MessageKinds {
|
||||
translationFinished = "translation",
|
||||
|
@ -18,11 +18,7 @@ export type Message = TranslationMessage | LanguageListMessage;
|
|||
if (window.location.host != "translate.google.com" || window.hasRun) return;
|
||||
window.hasRun = true;
|
||||
|
||||
const conn = browser.runtime.connect(<any>{
|
||||
name: "gtranslate_scraper_conn",
|
||||
});
|
||||
|
||||
const sendMessage = (m: Message) => conn.postMessage(m);
|
||||
const conn = new ContentScriptMessenger();
|
||||
|
||||
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
|
||||
const result = <HTMLDivElement>(
|
||||
|
@ -39,7 +35,6 @@ export type Message = TranslationMessage | LanguageListMessage;
|
|||
const isTranslating = () => result.classList.contains("translating");
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
console.log(mutations);
|
||||
const wasTranslating = mutations.some((m) =>
|
||||
m.oldValue!.split(" ").some((c) => c == "translating")
|
||||
);
|
||||
|
@ -54,22 +49,19 @@ export type Message = TranslationMessage | LanguageListMessage;
|
|||
});
|
||||
|
||||
const sendTranslation = () => {
|
||||
sendMessage({
|
||||
messageKind: MessageKinds.translationFinished,
|
||||
translation: {
|
||||
conn.runCommand("translationFinished", {
|
||||
src: src.value,
|
||||
result: result.innerText.trim(),
|
||||
languages: {
|
||||
srcLang: srcLang,
|
||||
dstLang: dstLang,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
conn.onMessage.addListener((to_translate: string) => {
|
||||
if (src.value == to_translate && !isTranslating()) sendTranslation();
|
||||
else src.value = to_translate;
|
||||
conn.addMessageListener("translate", (toTranslate) => {
|
||||
if (src.value == toTranslate && !isTranslating()) sendTranslation();
|
||||
else src.value = toTranslate;
|
||||
});
|
||||
|
||||
const getLanguages = () => {
|
||||
|
@ -83,8 +75,5 @@ export type Message = TranslationMessage | LanguageListMessage;
|
|||
return { code: code!, name: name! };
|
||||
});
|
||||
};
|
||||
sendMessage({
|
||||
messageKind: MessageKinds.languageList,
|
||||
languages: getLanguages(),
|
||||
});
|
||||
conn.runCommand("languageList", getLanguages());
|
||||
})();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
|
||||
import content_script from "./gtranslate_content_script.ts?file";
|
||||
import { Message, MessageKinds } from "./gtranslate_content_script";
|
||||
import { BackgroundMessenger } from "./gtranslate_commands";
|
||||
import { newPromise } from "../utils";
|
||||
|
||||
interface TranslationRequest {
|
||||
|
@ -12,42 +12,50 @@ interface TranslationRequest {
|
|||
}
|
||||
|
||||
export class GTranslateScraper {
|
||||
private conn?: Runtime.Port;
|
||||
private queue: Array<TranslationRequest> = [];
|
||||
private current?: TranslationRequest;
|
||||
private iframe = <HTMLIFrameElement>document.createElement("iframe");
|
||||
private current_lang: LanguagePair | undefined;
|
||||
|
||||
languages: Promise<Array<Language>>;
|
||||
lang: LanguagePair | undefined;
|
||||
msg: BackgroundMessenger;
|
||||
|
||||
constructor() {
|
||||
let languagesResolve: (value: Array<Language>) => void;
|
||||
[this.languages, languagesResolve] = newPromise<Array<Language>>();
|
||||
|
||||
browser.runtime.onConnect.addListener((p) => {
|
||||
if (p.name != "gtranslate_scraper_conn") return;
|
||||
this.conn = p;
|
||||
this.processFromQueue(p);
|
||||
p.onMessage.addListener((m: Message) => {
|
||||
switch (m.messageKind) {
|
||||
case MessageKinds.languageList:
|
||||
languagesResolve(m.languages);
|
||||
return;
|
||||
case MessageKinds.translationFinished:
|
||||
this.onTranslationRecieved(m.translation);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.loadIframe("de", "en"); //Sample languages so we can get the language list
|
||||
this.msg = new BackgroundMessenger();
|
||||
this.msg.addMessageListener("languageList", languagesResolve);
|
||||
this.msg.addMessageListener("translationFinished", (trans) =>
|
||||
this.onTranslationRecieved(trans)
|
||||
);
|
||||
|
||||
//This allows us to create the google translate iframe by removing the x-frame-options header
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
allowIFrameAccess,
|
||||
{
|
||||
urls: ["https://translate.google.com/?*"],
|
||||
types: ["sub_frame"],
|
||||
},
|
||||
["blocking", "responseHeaders"]
|
||||
);
|
||||
|
||||
this.ChangeLanguage("de", "en"); //Sample languages so we can get the language list
|
||||
}
|
||||
|
||||
private loadIframe(srcLangCode: string, dstLangCode: string) {
|
||||
private async ChangeLanguage(
|
||||
srcLangCode: string,
|
||||
dstLangCode: string
|
||||
): Promise<void> {
|
||||
if (!document.body.contains(this.iframe))
|
||||
document.body.appendChild(this.iframe);
|
||||
this.conn = undefined;
|
||||
|
||||
const [connectedPromise, onConnected] = newPromise<void>();
|
||||
|
||||
//Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId
|
||||
browser.contentScripts
|
||||
.register({
|
||||
matches: ["<all_urls>"],
|
||||
matches: ["https://translate.google.com/?*"],
|
||||
allFrames: true,
|
||||
js: [
|
||||
{
|
||||
|
@ -57,39 +65,60 @@ export class GTranslateScraper {
|
|||
runAt: "document_end",
|
||||
})
|
||||
.then((r) => {
|
||||
this.iframe.addEventListener("load", r.unregister, { once: true });
|
||||
this.iframe.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
r.unregister;
|
||||
this.msg.conn.then(() => onConnected());
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this.iframe.src = `https://translate.google.com/?op=translate&sl=${srcLangCode}&tl=${dstLangCode}`;
|
||||
});
|
||||
|
||||
//This allows us to create the google translate iframe by removing the x-frame-options header
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
allowIFrameAccess,
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
types: ["sub_frame"],
|
||||
},
|
||||
["blocking", "responseHeaders"]
|
||||
);
|
||||
return connectedPromise;
|
||||
}
|
||||
|
||||
private onTranslationRecieved(trans: Translation) {
|
||||
const current = this.queue.pop();
|
||||
if (current) current.resolveFn(trans);
|
||||
this.processFromQueue(this.conn!);
|
||||
if (this.current?.toTranslate !== trans.src)
|
||||
//I don't know how to code well so I gotta put in sanity checks like this to make debugging easier
|
||||
console.error(
|
||||
"Current request changed before OnTranslationRecieved was called. This should never happen"
|
||||
);
|
||||
if (this.current) this.current.resolveFn(trans);
|
||||
this.current = undefined;
|
||||
this.processFromQueue();
|
||||
}
|
||||
|
||||
private processFromQueue(conn: Runtime.Port) {
|
||||
const request = this.queue.slice(-1).pop();
|
||||
if (!request) return;
|
||||
private async processFromQueue() {
|
||||
if (this.current) return;
|
||||
this.current = this.queue.pop();
|
||||
if (!this.current) return;
|
||||
|
||||
if (this.lang != request.lang) {
|
||||
this.lang = request.lang;
|
||||
|
||||
this.loadIframe(this.lang.srcLang.code, this.lang.dstLang.code);
|
||||
} else conn.postMessage(request.toTranslate);
|
||||
this.processCurrent();
|
||||
}
|
||||
|
||||
translate(
|
||||
private async processCurrent() {
|
||||
if (!this.current)
|
||||
throw "ProcessCurrent called while current translation request is undefined";
|
||||
const cur = this.current;
|
||||
|
||||
let promise = Promise.resolve();
|
||||
if (this.current_lang != this.current.lang) {
|
||||
this.current_lang = cur.lang;
|
||||
|
||||
promise = this.ChangeLanguage(
|
||||
this.current_lang.srcLang.code,
|
||||
this.current_lang.dstLang.code
|
||||
);
|
||||
}
|
||||
await promise;
|
||||
if (cur == this.current) this.msg.runCommand("translate", cur.toTranslate);
|
||||
else
|
||||
throw "The current request changed while processCurrent was running. This should never happen";
|
||||
}
|
||||
|
||||
async translate(
|
||||
toTranslate: string,
|
||||
langs: LanguagePair,
|
||||
tabID?: number
|
||||
|
@ -119,9 +148,14 @@ export class GTranslateScraper {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
this.queue.push(req);
|
||||
if (this.queue.length == 1 && this.conn) this.processFromQueue(this.conn);
|
||||
|
||||
this.queue.push(req);
|
||||
|
||||
if (this.queue.length == 1 && !this.current) {
|
||||
this.current = this.queue.pop();
|
||||
await this.msg.conn;
|
||||
this.processCurrent();
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Communicator, commandFunction, commandList } from "./communicator";
|
||||
import { browser } from "webextension-polyfill-ts";
|
||||
|
||||
export interface commands extends commandList {
|
||||
interface BackgroundCommands extends commandList {
|
||||
setEnabled: setEnabled;
|
||||
getEnabled: getEnabled;
|
||||
translate: translate;
|
||||
|
@ -12,6 +12,10 @@ export interface commands extends commandList {
|
|||
setCurrentLanguages: setCurrentLanguages;
|
||||
}
|
||||
|
||||
interface FrontendCommands extends commandList {
|
||||
setEnabled: setEnabled;
|
||||
}
|
||||
|
||||
interface setEnabled extends commandFunction {
|
||||
args: boolean;
|
||||
}
|
||||
|
@ -46,14 +50,17 @@ interface setCurrentLanguages extends commandFunction {
|
|||
args: Partial<LanguagePair>;
|
||||
}
|
||||
|
||||
export class BackgroundMessenger extends Communicator<commands> {
|
||||
export class BackgroundMessenger extends Communicator<
|
||||
BackgroundCommands,
|
||||
FrontendCommands
|
||||
> {
|
||||
constructor() {
|
||||
super();
|
||||
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
|
||||
}
|
||||
runCommand<K extends keyof commands>(
|
||||
runCommand<K extends keyof BackgroundCommands>(
|
||||
command: K,
|
||||
args: commands[K]["args"],
|
||||
args: BackgroundCommands[K]["args"],
|
||||
tabID: number
|
||||
) {
|
||||
const msg = this.getCommandMessage(command, args);
|
||||
|
@ -61,16 +68,19 @@ export class BackgroundMessenger extends Communicator<commands> {
|
|||
}
|
||||
}
|
||||
|
||||
export class FrontendMessenger extends Communicator<commands> {
|
||||
export class FrontendMessenger extends Communicator<
|
||||
FrontendCommands,
|
||||
BackgroundCommands
|
||||
> {
|
||||
constructor() {
|
||||
super();
|
||||
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
|
||||
}
|
||||
|
||||
runCommand<K extends keyof commands>(
|
||||
runCommand<K extends keyof BackgroundCommands>(
|
||||
command: K,
|
||||
args: commands[K]["args"]
|
||||
): commands[K]["return"] {
|
||||
args: BackgroundCommands[K]["args"]
|
||||
): BackgroundCommands[K]["return"] {
|
||||
const message = this.getCommandMessage(command, args);
|
||||
return browser.runtime.sendMessage(message);
|
||||
}
|
||||
|
|
|
@ -16,35 +16,47 @@ interface commandMessage<T extends commandList, K extends keyof T> {
|
|||
|
||||
declare type listener<T extends commandList, K extends keyof T> = (
|
||||
args: T[K]["args"],
|
||||
s: Runtime.MessageSender
|
||||
) => Promise<T[K]["return"]>;
|
||||
s?: Runtime.MessageSender
|
||||
) => Promise<T[K]["return"]> | T[K]["return"];
|
||||
|
||||
export abstract class Communicator<T extends commandList> {
|
||||
private listeners = new Map<keyof T, listener<T, keyof T>>();
|
||||
export abstract class Communicator<
|
||||
RecvCommands extends commandList,
|
||||
SendCommands extends commandList
|
||||
> {
|
||||
private listeners = new Map<
|
||||
keyof RecvCommands,
|
||||
listener<RecvCommands, keyof RecvCommands>
|
||||
>();
|
||||
|
||||
abstract runCommand<K extends keyof T>(
|
||||
abstract async runCommand<K extends keyof SendCommands>(
|
||||
command: K,
|
||||
args: T[K]["args"],
|
||||
args: SendCommands[K]["args"],
|
||||
...rest: any[]
|
||||
): Promise<T[K]["return"]>;
|
||||
): Promise<SendCommands[K]["return"]>;
|
||||
|
||||
addMessageListener<K extends keyof T>(command: K, callback: listener<T, K>) {
|
||||
addMessageListener<K extends keyof RecvCommands>(
|
||||
command: K,
|
||||
callback: listener<RecvCommands, K>
|
||||
) {
|
||||
this.listeners.set(command, <any>callback);
|
||||
}
|
||||
|
||||
getCommandMessage<K extends keyof T>(
|
||||
protected getCommandMessage<K extends keyof RecvCommands>(
|
||||
command: K,
|
||||
...args: T[K]["args"]
|
||||
): commandMessage<T, K> {
|
||||
...args: RecvCommands[K]["args"]
|
||||
): commandMessage<RecvCommands, K> {
|
||||
return { name: command, args: args };
|
||||
}
|
||||
|
||||
onMessage<K extends keyof T>(
|
||||
m: commandMessage<T, K>,
|
||||
s: Runtime.MessageSender
|
||||
protected onMessage<K extends keyof RecvCommands>(
|
||||
m: commandMessage<RecvCommands, K>,
|
||||
s?: Runtime.MessageSender
|
||||
) {
|
||||
let listener = this.listeners.get(m.name);
|
||||
let args: [T[keyof T]["args"], Runtime.MessageSender] = m.args;
|
||||
let args: [
|
||||
RecvCommands[keyof RecvCommands]["args"],
|
||||
Runtime.MessageSender
|
||||
] = m.args;
|
||||
if (args[0] === undefined) args.pop();
|
||||
args.push(s);
|
||||
if (listener) return listener.apply(null, args);
|
||||
|
|
Loading…
Reference in a new issue