diff options
Diffstat (limited to 'ext/bg/js')
-rw-r--r-- | ext/bg/js/audio-uri-builder.js (renamed from ext/bg/js/audio.js) | 148 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 57 | ||||
-rw-r--r-- | ext/bg/js/settings/audio.js | 4 |
3 files changed, 110 insertions, 99 deletions
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio-uri-builder.js index c94121ae..15cea995 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio-uri-builder.js @@ -18,8 +18,49 @@ /*global jpIsStringEntirelyKana*/ -const audioUrlBuilders = new Map([ - ['jpod101', async (definition) => { +class AudioUriBuilder { + constructor() { + 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)] + ]); + } + + normalizeUrl(url, baseUrl, basePath) { + if (url) { + if (url[0] === '/') { + if (url.length >= 2 && url[1] === '/') { + // Begins with "//" + url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url; + } else { + // Begins with "/" + url = baseUrl + url; + } + } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) { + // No URI scheme => relative path + url = baseUrl + basePath + url; + } + } + return url; + } + + async getUri(definition, source, options) { + const handler = this._getUrlHandlers.get(source); + if (typeof handler === 'function') { + try { + return await handler(definition, options); + } catch (e) { + // NOP + } + } + return null; + } + + async _getUriJpod101(definition) { let kana = definition.reading; let kanji = definition.expression; @@ -37,8 +78,9 @@ const audioUrlBuilders = new Map([ } return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`; - }], - ['jpod101-alternate', async (definition) => { + } + + async _getUriJpod101Alternate(definition) { const response = await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'); @@ -54,7 +96,7 @@ const audioUrlBuilders = new Map([ const url = row.querySelector('audio>source[src]').getAttribute('src'); const reading = row.getElementsByClassName('dc-vocab_kana').item(0).textContent; if (url && reading && (!definition.reading || definition.reading === reading)) { - return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); + return this.normalizeUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); } } catch (e) { // NOP @@ -62,8 +104,9 @@ const audioUrlBuilders = new Map([ } throw new Error('Failed to find audio URL'); - }], - ['jisho', async (definition) => { + } + + async _getUriJisho(definition) { const response = await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', `https://jisho.org/search/${definition.expression}`); @@ -78,7 +121,7 @@ const audioUrlBuilders = new Map([ if (audio !== null) { const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); if (url) { - return audioUrlNormalize(url, 'https://jisho.org', '/search/'); + return this.normalizeUrl(url, 'https://jisho.org', '/search/'); } } } catch (e) { @@ -86,99 +129,28 @@ const audioUrlBuilders = new Map([ } throw new Error('Failed to find audio URL'); - }], - ['text-to-speech', async (definition, options) => { + } + + async _getUriTextToSpeech(definition, options) { const voiceURI = options.audio.textToSpeechVoice; if (!voiceURI) { throw new Error('No voice'); } return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; - }], - ['text-to-speech-reading', async (definition, options) => { + } + + async _getUriTextToSpeechReading(definition, options) { const voiceURI = options.audio.textToSpeechVoice; if (!voiceURI) { throw new Error('No voice'); } return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; - }], - ['custom', async (definition, options) => { - const customSourceUrl = options.audio.customSourceUrl; - return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); - }] -]); - -async function audioGetUrl(definition, mode, options, download) { - const handler = audioUrlBuilders.get(mode); - if (typeof handler === 'function') { - try { - return await handler(definition, options, download); - } catch (e) { - // NOP - } } - return null; -} -function audioUrlNormalize(url, baseUrl, basePath) { - if (url) { - if (url[0] === '/') { - if (url.length >= 2 && url[1] === '/') { - // Begins with "//" - url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url; - } else { - // Begins with "/" - url = baseUrl + url; - } - } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) { - // No URI scheme => relative path - url = baseUrl + basePath + url; - } - } - return url; -} - -function audioBuildFilename(definition) { - if (definition.reading || definition.expression) { - let filename = 'yomichan'; - if (definition.reading) { - filename += `_${definition.reading}`; - } - if (definition.expression) { - filename += `_${definition.expression}`; - } - - return filename += '.mp3'; - } - return null; -} - -async function audioInject(definition, fields, sources, optionsContext, audioSystem) { - let usesAudio = false; - for (const fieldValue of Object.values(fields)) { - if (fieldValue.includes('{audio}')) { - usesAudio = true; - break; - } - } - - if (!usesAudio) { - return true; - } - - try { - const expressions = definition.expressions; - const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; - - const {uri} = await audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); - const filename = audioBuildFilename(audioSourceDefinition); - if (filename !== null) { - definition.audio = {url: uri, filename}; - } - - return true; - } catch (e) { - return false; + async _getUriCustom(definition, options) { + const customSourceUrl = options.audio.customSourceUrl; + return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); } } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d0d53a36..349fb4eb 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -21,9 +21,8 @@ conditionsTestValue, profileConditionsDescriptor handlebarsRenderDynamic requestText, requestJson, optionsLoad dictConfigured, dictTermsSort, dictEnabledSet -audioGetUrl, audioInject jpConvertReading, jpDistributeFuriganaInflected, jpKatakanaToHiragana -AnkiNoteBuilder, AudioSystem, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ +AnkiNoteBuilder, AudioSystem, AudioUriBuilder, Translator, AnkiConnect, AnkiNull, Mecab, BackendApiForwarder, JsonSchema, ClipboardMonitor*/ class Backend { constructor() { @@ -36,6 +35,7 @@ class Backend { this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)}); + this.audioUriBuilder = new AudioUriBuilder(); this.optionsContext = { depth: 0, url: window.location.href @@ -67,7 +67,7 @@ class Backend { ['noteView', this._onApiNoteView.bind(this)], ['templateRender', this._onApiTemplateRender.bind(this)], ['commandExec', this._onApiCommandExec.bind(this)], - ['audioGetUrl', this._onApiAudioGetUrl.bind(this)], + ['audioGetUri', this._onApiAudioGetUri.bind(this)], ['screenshotGet', this._onApiScreenshotGet.bind(this)], ['forward', this._onApiForward.bind(this)], ['frameInformationGet', this._onApiFrameInformationGet.bind(this)], @@ -434,12 +434,11 @@ class Backend { const templates = this.defaultAnkiFieldTemplates; if (mode !== 'kanji') { - await audioInject( + await this._audioInject( definition, options.anki.terms.fields, options.audio.sources, - optionsContext, - this.audioSystem + optionsContext ); } @@ -514,9 +513,9 @@ class Backend { return this._runCommand(command, params); } - async _onApiAudioGetUrl({definition, source, optionsContext}) { + async _onApiAudioGetUri({definition, source, optionsContext}) { const options = this.getOptions(optionsContext); - return await audioGetUrl(definition, source, options); + return await this.audioUriBuilder.getUri(definition, source, options); } _onApiScreenshotGet({options}, sender) { @@ -772,7 +771,36 @@ class Backend { } const options = this.getOptions(optionsContext); - return await audioGetUrl(definition, source, options); + return await this.audioUriBuilder.getUri(definition, source, options); + } + + async _audioInject(definition, fields, sources, optionsContext) { + let usesAudio = false; + for (const fieldValue of Object.values(fields)) { + if (fieldValue.includes('{audio}')) { + usesAudio = true; + break; + } + } + + if (!usesAudio) { + return true; + } + + try { + const expressions = definition.expressions; + const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; + + const {uri} = await this.audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); + const filename = this._createInjectedAudioFileName(audioSourceDefinition); + if (filename !== null) { + definition.audio = {url: uri, filename}; + } + + return true; + } catch (e) { + return false; + } } async _injectScreenshot(definition, fields, screenshot) { @@ -815,6 +843,17 @@ class Backend { return handlebarsRenderDynamic(template, data); } + _createInjectedAudioFileName(definition) { + const {reading, expression} = definition; + if (!reading && !expression) { return null; } + + let filename = 'yomichan'; + if (reading) { filename += `_${reading}`; } + if (expression) { filename += `_${expression}`; } + filename += '.mp3'; + return filename; + } + static _getTabUrl(tab) { return new Promise((resolve) => { chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 6f581d9b..c825be6b 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -/*global getOptionsContext, getOptionsMutable, settingsSaveOptions, apiAudioGetUrl +/*global getOptionsContext, getOptionsMutable, settingsSaveOptions, apiAudioGetUri AudioSystem, AudioSourceUI*/ let audioSourceUI = null; @@ -26,7 +26,7 @@ async function audioSettingsInitialize() { audioSystem = new AudioSystem({ getAudioUri: async (definition, source) => { const optionsContext = getOptionsContext(); - return await apiAudioGetUrl(definition, source, optionsContext); + return await apiAudioGetUri(definition, source, optionsContext); } }); |