aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-09-26 13:41:26 -0400
committerGitHub <noreply@github.com>2020-09-26 13:41:26 -0400
commit0b51488f1f639885b518fabf683b70db577afa67 (patch)
tree5fca858efdecb345e8bcabec5ff4f727c7f1dc20 /ext/bg/js
parent22932e02cbd1fb16d0180c79a3c9b4d1790fbfda (diff)
Audio system refactor (#858)
* Refactor AudioUriBuilder * Add downloadAudio function * Refactor AudioSystem * Update API usage * Rename file * Update scripts * Add prepare calls
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/audio-downloader.js (renamed from ext/bg/js/audio-uri-builder.js)142
-rw-r--r--ext/bg/js/backend.js32
-rw-r--r--ext/bg/js/settings/audio-controller.js9
3 files changed, 124 insertions, 59 deletions
diff --git a/ext/bg/js/audio-uri-builder.js b/ext/bg/js/audio-downloader.js
index 78ea9caa..8dd5d10f 100644
--- a/ext/bg/js/audio-uri-builder.js
+++ b/ext/bg/js/audio-downloader.js
@@ -20,20 +20,54 @@
* jp
*/
-class AudioUriBuilder {
+class AudioDownloader {
constructor({requestBuilder}) {
this._requestBuilder = requestBuilder;
- this._getUrlHandlers = new Map([
- ['jpod101', this._getUriJpod101.bind(this)],
- ['jpod101-alternate', this._getUriJpod101Alternate.bind(this)],
- ['jisho', this._getUriJisho.bind(this)],
- ['text-to-speech', this._getUriTextToSpeech.bind(this)],
- ['text-to-speech-reading', this._getUriTextToSpeechReading.bind(this)],
- ['custom', this._getUriCustom.bind(this)]
+ this._getInfoHandlers = new Map([
+ ['jpod101', this._getInfoJpod101.bind(this)],
+ ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)],
+ ['jisho', this._getInfoJisho.bind(this)],
+ ['text-to-speech', this._getInfoTextToSpeech.bind(this)],
+ ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)],
+ ['custom', this._getInfoCustom.bind(this)]
]);
}
- normalizeUrl(url, baseUrl, basePath) {
+ async getInfo(source, expression, reading, details) {
+ const handler = this._getInfoHandlers.get(source);
+ if (typeof handler === 'function') {
+ try {
+ return await handler(expression, reading, details);
+ } catch (e) {
+ // NOP
+ }
+ }
+ return null;
+ }
+
+ async downloadAudio(sources, expression, reading, details) {
+ for (const source of sources) {
+ const info = await this.getInfo(source, expression, reading, details);
+ if (info === null) { continue; }
+
+ switch (info.type) {
+ case 'url':
+ try {
+ const {details: {url}} = info;
+ return await this._downloadAudioFromUrl(url);
+ } catch (e) {
+ // NOP
+ }
+ break;
+ }
+ }
+
+ throw new Error('Could not download audio');
+ }
+
+ // Private
+
+ _normalizeUrl(url, baseUrl, basePath) {
if (url) {
if (url[0] === '/') {
if (url.length >= 2 && url[1] === '/') {
@@ -51,19 +85,7 @@ class AudioUriBuilder {
return url;
}
- async getUri(source, expression, reading, details) {
- const handler = this._getUrlHandlers.get(source);
- if (typeof handler === 'function') {
- try {
- return await handler(expression, reading, details);
- } catch (e) {
- // NOP
- }
- }
- return null;
- }
-
- async _getUriJpod101(expression, reading) {
+ async _getInfoJpod101(expression, reading) {
let kana = reading;
let kanji = expression;
@@ -80,10 +102,11 @@ class AudioUriBuilder {
params.push(`kana=${encodeURIComponent(kana)}`);
}
- return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
+ const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
+ return {type: 'url', details: {url}};
}
- async _getUriJpod101Alternate(expression, reading) {
+ async _getInfoJpod101Alternate(expression, reading) {
const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post';
const data = `post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(expression)}&vulgar=true`;
const response = await this._requestBuilder.fetchAnonymous(fetchUrl, {
@@ -109,7 +132,7 @@ class AudioUriBuilder {
const source = dom.getElementByTagName('source', audio);
if (source === null) { continue; }
- const url = dom.getAttribute(source, 'src');
+ let url = dom.getAttribute(source, 'src');
if (url === null) { continue; }
const htmlReadings = dom.getElementsByClassName('dc-vocab_kana');
@@ -117,7 +140,8 @@ class AudioUriBuilder {
const htmlReading = dom.getTextContent(htmlReadings[0]);
if (htmlReading && (!reading || reading === htmlReading)) {
- return this.normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
+ url = this._normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
+ return {type: 'url', details: {url}};
}
} catch (e) {
// NOP
@@ -127,7 +151,7 @@ class AudioUriBuilder {
throw new Error('Failed to find audio URL');
}
- async _getUriJisho(expression, reading) {
+ async _getInfoJisho(expression, reading) {
const fetchUrl = `https://jisho.org/search/${expression}`;
const response = await this._requestBuilder.fetchAnonymous(fetchUrl, {
method: 'GET',
@@ -145,9 +169,10 @@ class AudioUriBuilder {
if (audio !== null) {
const source = dom.getElementByTagName('source', audio);
if (source !== null) {
- const url = dom.getAttribute(source, 'src');
+ let url = dom.getAttribute(source, 'src');
if (url !== null) {
- return this.normalizeUrl(url, 'https://jisho.org', '/search/');
+ url = this._normalizeUrl(url, 'https://jisho.org', '/search/');
+ return {type: 'url', details: {url}};
}
}
}
@@ -158,25 +183,72 @@ class AudioUriBuilder {
throw new Error('Failed to find audio URL');
}
- async _getUriTextToSpeech(expression, reading, {textToSpeechVoice}) {
+ async _getInfoTextToSpeech(expression, reading, {textToSpeechVoice}) {
if (!textToSpeechVoice) {
throw new Error('No voice');
}
- return `tts:?text=${encodeURIComponent(expression)}&voice=${encodeURIComponent(textToSpeechVoice)}`;
+ return {type: 'tts', details: {text: expression, voice: textToSpeechVoice}};
}
- async _getUriTextToSpeechReading(expression, reading, {textToSpeechVoice}) {
+ async _getInfoTextToSpeechReading(expression, reading, {textToSpeechVoice}) {
if (!textToSpeechVoice) {
throw new Error('No voice');
}
- return `tts:?text=${encodeURIComponent(reading || expression)}&voice=${encodeURIComponent(textToSpeechVoice)}`;
+ return {type: 'tts', details: {text: reading || expression, voice: textToSpeechVoice}};
}
- async _getUriCustom(expression, reading, {customSourceUrl}) {
+ async _getInfoCustom(expression, reading, {customSourceUrl}) {
if (typeof customSourceUrl !== 'string') {
throw new Error('No custom URL defined');
}
const data = {expression, reading};
- return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(data, m1) ? `${data[m1]}` : m0));
+ const url = customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(data, m1) ? `${data[m1]}` : m0));
+ return {type: 'url', details: {url}};
+ }
+
+ async _downloadAudioFromUrl(url) {
+ const response = await this._requestBuilder.fetchAnonymous(url, {
+ method: 'GET',
+ mode: 'cors',
+ cache: 'default',
+ credentials: 'omit',
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer'
+ });
+
+ if (!response.ok) {
+ throw new Error(`Invalid response: ${response.status}`);
+ }
+
+ const arrayBuffer = await response.arrayBuffer();
+
+ if (!await this._isAudioBinaryValid(arrayBuffer)) {
+ throw new Error('Could not retrieve audio');
+ }
+
+ return this._arrayBufferToBase64(arrayBuffer);
+ }
+
+ async _isAudioBinaryValid(arrayBuffer) {
+ const digest = await this._arrayBufferDigest(arrayBuffer);
+ switch (digest) {
+ case 'ae6398b5a27bc8c0a771df6c907ade794be15518174773c58c7c7ddd17098906': // jpod101 invalid audio
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ async _arrayBufferDigest(arrayBuffer) {
+ const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer)));
+ let digest = '';
+ for (const byte of hash) {
+ digest += byte.toString(16).padStart(2, '0');
+ }
+ return digest;
+ }
+
+ _arrayBufferToBase64(arrayBuffer) {
+ return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
}
}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 91188fdc..b9d85b84 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -17,8 +17,7 @@
/* global
* AnkiConnect
- * AudioSystem
- * AudioUriBuilder
+ * AudioDownloader
* ClipboardMonitor
* ClipboardReader
* DictionaryDatabase
@@ -54,14 +53,9 @@ class Backend {
this._profileConditionsUtil = new ProfileConditions();
this._defaultAnkiFieldTemplates = null;
this._requestBuilder = new RequestBuilder();
- this._audioUriBuilder = new AudioUriBuilder({
+ this._audioDownloader = new AudioDownloader({
requestBuilder: this._requestBuilder
});
- this._audioSystem = new AudioSystem({
- audioUriBuilder: this._audioUriBuilder,
- requestBuilder: this._requestBuilder,
- useCache: false
- });
this._optionsUtil = new OptionsUtil();
this._searchPopupTabId = null;
@@ -91,7 +85,8 @@ class Backend {
['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}],
['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}],
['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}],
- ['audioGetUri', {async: true, contentScript: true, handler: this._onApiAudioGetUri.bind(this)}],
+ ['getDefinitionAudioInfo', {async: true, contentScript: true, handler: this._onApiGetDefinitionAudioInfo.bind(this)}],
+ ['downloadDefinitionAudio', {async: true, contentScript: true, handler: this._onApiDownloadDefinitionAudio.bind(this)}],
['screenshotGet', {async: true, contentScript: true, handler: this._onApiScreenshotGet.bind(this)}],
['sendMessageToFrame', {async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this)}],
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
@@ -117,7 +112,6 @@ class Backend {
['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}],
['getOrCreateSearchPopup', {async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this)}],
['isTabSearchPopup', {async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this)}],
- ['getDefinitionAudio', {async: true, contentScript: true, handler: this._onApiGetDefinitionAudio.bind(this)}],
['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}]
]);
this._messageHandlersWithProgress = new Map([
@@ -479,8 +473,12 @@ class Backend {
return this._runCommand(command, params);
}
- async _onApiAudioGetUri({source, expression, reading, details}) {
- return await this._audioUriBuilder.getUri(source, expression, reading, details);
+ async _onApiGetDefinitionAudioInfo({source, expression, reading, details}) {
+ return await this._audioDownloader.getInfo(source, expression, reading, details);
+ }
+
+ async _onApiDownloadDefinitionAudio({sources, expression, reading, details}) {
+ return await this._downloadDefinitionAudio(sources, expression, reading, details);
}
_onApiScreenshotGet({options}, sender) {
@@ -728,10 +726,6 @@ class Backend {
return (tab !== null);
}
- async _onApiGetDefinitionAudio({sources, expression, reading, details}) {
- return this._getDefinitionAudio(sources, expression, reading, details);
- }
-
_onApiTriggerDatabaseUpdated({type, cause}) {
this._triggerDatabaseUpdated(type, cause);
}
@@ -1511,8 +1505,8 @@ class Backend {
}
}
- async _getDefinitionAudio(sources, expression, reading, details) {
- return await this._audioSystem.getDefinitionAudio(sources, expression, reading, details);
+ async _downloadDefinitionAudio(sources, expression, reading, details) {
+ return await this._audioDownloader.downloadAudio(sources, expression, reading, details);
}
async _injectAnkNoteMedia(ankiConnect, expression, reading, timestamp, audioDetails, screenshotDetails, clipboardImage) {
@@ -1548,7 +1542,7 @@ class Backend {
fileName = this._replaceInvalidFileNameCharacters(fileName);
const {sources, customSourceUrl} = details;
- const {audio: data} = await this._getDefinitionAudio(
+ const data = await this._downloadDefinitionAudio(
sources,
expression,
reading,
diff --git a/ext/bg/js/settings/audio-controller.js b/ext/bg/js/settings/audio-controller.js
index d389acb5..bd7951f7 100644
--- a/ext/bg/js/settings/audio-controller.js
+++ b/ext/bg/js/settings/audio-controller.js
@@ -22,17 +22,16 @@
class AudioController {
constructor(settingsController) {
this._settingsController = settingsController;
- this._audioSystem = null;
+ this._audioSystem = new AudioSystem({
+ cacheSize: 0
+ });
this._audioSourceContainer = null;
this._audioSourceAddButton = null;
this._audioSourceEntries = [];
}
async prepare() {
- this._audioSystem = new AudioSystem({
- audioUriBuilder: null,
- useCache: true
- });
+ this._audioSystem.prepare();
this._audioSourceContainer = document.querySelector('.audio-source-list');
this._audioSourceAddButton = document.querySelector('.audio-source-add');