diff options
author | StefanVukovic99 <stefanvukovic44@gmail.com> | 2024-06-27 18:08:42 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-27 16:08:42 +0000 |
commit | 603c2c7e1b50d8b06c06848c3e83d241da9437e6 (patch) | |
tree | 2e04bdb57475c630170315678d789751bfeb6444 /ext/js | |
parent | 4e3f23e942252dacb31780de30f0233eccf1d9f8 (diff) |
add lingua libre audio source (#1129)
* add lingua libre audio source
* mvp
* run file requests in parallel
* remove redundant language var
* redundant api function
---------
Co-authored-by: Cashew <52880648+cashewnuttynuts@users.noreply.github.com>
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/background/backend.js | 7 | ||||
-rw-r--r-- | ext/js/comm/api.js | 5 | ||||
-rw-r--r-- | ext/js/data/anki-note-builder.js | 4 | ||||
-rw-r--r-- | ext/js/display/display-anki.js | 14 | ||||
-rw-r--r-- | ext/js/display/display-audio.js | 4 | ||||
-rw-r--r-- | ext/js/display/display.js | 14 | ||||
-rw-r--r-- | ext/js/language/language-descriptors.js | 31 | ||||
-rwxr-xr-x | ext/js/language/languages.js | 4 | ||||
-rw-r--r-- | ext/js/media/audio-downloader.js | 96 | ||||
-rw-r--r-- | ext/js/pages/settings/audio-controller.js | 2 |
10 files changed, 133 insertions, 48 deletions
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index a468a3f6..96e8206b 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -676,8 +676,8 @@ export class Backend { } /** @type {import('api').ApiHandler<'getTermAudioInfoList'>} */ - async _onApiGetTermAudioInfoList({source, term, reading}) { - return await this._audioDownloader.getTermAudioInfoList(source, term, reading); + async _onApiGetTermAudioInfoList({source, term, reading, languageSummary}) { + return await this._audioDownloader.getTermAudioInfoList(source, term, reading, languageSummary); } /** @type {import('api').ApiHandler<'sendMessageToFrame'>} */ @@ -2171,7 +2171,7 @@ export class Backend { const {term, reading} = definitionDetails; if (term.length === 0 && reading.length === 0) { return null; } - const {sources, preferredAudioIndex, idleTimeout} = details; + const {sources, preferredAudioIndex, idleTimeout, languageSummary} = details; let data; let contentType; try { @@ -2181,6 +2181,7 @@ export class Backend { term, reading, idleTimeout, + languageSummary, )); } catch (e) { const error = this._getAudioDownloadError(e); diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 7a98feb1..952b5bfa 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -147,10 +147,11 @@ export class API { * @param {import('api').ApiParam<'getTermAudioInfoList', 'source'>} source * @param {import('api').ApiParam<'getTermAudioInfoList', 'term'>} term * @param {import('api').ApiParam<'getTermAudioInfoList', 'reading'>} reading + * @param {import('api').ApiParam<'getTermAudioInfoList', 'languageSummary'>} languageSummary * @returns {Promise<import('api').ApiReturn<'getTermAudioInfoList'>>} */ - getTermAudioInfoList(source, term, reading) { - return this._invoke('getTermAudioInfoList', {source, term, reading}); + getTermAudioInfoList(source, term, reading, languageSummary) { + return this._invoke('getTermAudioInfoList', {source, term, reading, languageSummary}); } /** diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 38588439..7f7fe3a7 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -428,8 +428,8 @@ export class AnkiNoteBuilder { if (injectAudio && dictionaryEntryDetails.type !== 'kanji') { const audioOptions = mediaOptions.audio; if (typeof audioOptions === 'object' && audioOptions !== null) { - const {sources, preferredAudioIndex, idleTimeout} = audioOptions; - audioDetails = {sources, preferredAudioIndex, idleTimeout}; + const {sources, preferredAudioIndex, idleTimeout, languageSummary} = audioOptions; + audioDetails = {sources, preferredAudioIndex, idleTimeout, languageSummary}; } } if (injectScreenshot) { diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index fc242549..b3b05408 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -193,7 +193,11 @@ export class DisplayAnki { */ _onOptionsUpdated({options}) { const { - general: {resultOutputMode, glossaryLayoutMode, compactTags}, + general: { + resultOutputMode, + glossaryLayoutMode, + compactTags, + }, dictionaries, anki: { tags, @@ -883,7 +887,13 @@ export class DisplayAnki { _getAnkiNoteMediaAudioDetails(details) { if (details.type !== 'term') { return null; } const {sources, preferredAudioIndex} = this._displayAudio.getAnkiNoteMediaAudioDetails(details.term, details.reading); - return {sources, preferredAudioIndex, idleTimeout: this._audioDownloadIdleTimeout}; + const languageSummary = this._display.getLanguageSummary(); + return { + sources, + preferredAudioIndex, + idleTimeout: this._audioDownloadIdleTimeout, + languageSummary, + }; } // View note functions diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index c7e08ffe..0d1ca029 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -57,6 +57,7 @@ export class DisplayAudio { ['jpod101', 'JapanesePod101'], ['jpod101-alternate', 'JapanesePod101 (Alternate)'], ['jisho', 'Jisho.org'], + ['lingua-libre', 'Lingua Libre'], ['text-to-speech', 'Text-to-speech'], ['text-to-speech-reading', 'Text-to-speech (Kana reading)'], ['custom', 'Custom URL'], @@ -677,7 +678,8 @@ export class DisplayAudio { */ async _getTermAudioInfoList(source, term, reading) { const sourceData = this._getSourceData(source); - const infoList = await this._display.application.api.getTermAudioInfoList(sourceData, term, reading); + const languageSummary = this._display.getLanguageSummary(); + const infoList = await this._display.application.api.getTermAudioInfoList(sourceData, term, reading, languageSummary); return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null})); } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index f86d7b8c..3d18e416 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -192,6 +192,8 @@ export class Display extends EventDispatcher { this._onMenuButtonMenuCloseBind = this._onMenuButtonMenuClose.bind(this); /** @type {ThemeController} */ this._themeController = new ThemeController(document.documentElement); + /** @type {import('language').LanguageSummary[]} */ + this._languageSummaries = []; /* eslint-disable @stylistic/no-multi-spaces */ this._hotkeyHandler.registerActions([ @@ -316,6 +318,8 @@ export class Display extends EventDispatcher { documentElement.dataset.browser = browser; } + this._languageSummaries = await this._application.api.getLanguageSummaries(); + // Prepare await this._hotkeyHelpController.prepare(this._application.api); await this._displayGenerator.prepare(); @@ -398,6 +402,16 @@ export class Display extends EventDispatcher { } /** + * @returns {import('language').LanguageSummary} + * @throws {Error} + */ + getLanguageSummary() { + if (this._options === null) { throw new Error('Options is null'); } + const language = this._options.general.language; + return /** @type {import('language').LanguageSummary} */ (this._languageSummaries.find(({iso}) => iso === language)); + } + + /** * @returns {import('settings').OptionsContext} */ getOptionsContext() { diff --git a/ext/js/language/language-descriptors.js b/ext/js/language/language-descriptors.js index f9fb4f09..2e8ece55 100644 --- a/ext/js/language/language-descriptors.js +++ b/ext/js/language/language-descriptors.js @@ -50,6 +50,7 @@ const capitalizationPreprocessors = { const languageDescriptors = [ { iso: 'ar', + iso639_3: 'ara', name: 'Arabic', exampleText: 'قَرَأَ', textPreprocessors: { @@ -58,6 +59,7 @@ const languageDescriptors = [ }, { iso: 'de', + iso639_3: 'deu', name: 'German', exampleText: 'gelesen', textPreprocessors: { @@ -68,12 +70,14 @@ const languageDescriptors = [ }, { iso: 'el', + iso639_3: 'ell', name: 'Greek', exampleText: 'διαβάζω', textPreprocessors: capitalizationPreprocessors, }, { iso: 'en', + iso639_3: 'eng', name: 'English', exampleText: 'read', textPreprocessors: capitalizationPreprocessors, @@ -81,6 +85,7 @@ const languageDescriptors = [ }, { iso: 'es', + iso639_3: 'spa', name: 'Spanish', exampleText: 'leer', textPreprocessors: capitalizationPreprocessors, @@ -88,6 +93,7 @@ const languageDescriptors = [ }, { iso: 'fa', + iso639_3: 'fas', name: 'Persian', exampleText: 'خواندن', textPreprocessors: { @@ -96,18 +102,21 @@ const languageDescriptors = [ }, { iso: 'fi', + iso639_3: 'fin', name: 'Finnish', exampleText: 'lukea', textPreprocessors: capitalizationPreprocessors, }, { iso: 'fr', + iso639_3: 'fra', name: 'French', exampleText: 'lire', textPreprocessors: capitalizationPreprocessors, }, { iso: 'grc', + iso639_3: 'grc', name: 'Ancient Greek', exampleText: 'γράφω', textPreprocessors: { @@ -117,24 +126,28 @@ const languageDescriptors = [ }, { iso: 'hu', + iso639_3: 'hun', name: 'Hungarian', exampleText: 'olvasni', textPreprocessors: capitalizationPreprocessors, }, { iso: 'id', + iso639_3: 'ind', name: 'Indonesian', exampleText: 'membaca', textPreprocessors: capitalizationPreprocessors, }, { iso: 'it', + iso639_3: 'ita', name: 'Italian', exampleText: 'leggere', textPreprocessors: capitalizationPreprocessors, }, { iso: 'la', + iso639_3: 'lat', name: 'Latin', exampleText: 'legere', textPreprocessors: { @@ -145,11 +158,13 @@ const languageDescriptors = [ }, { iso: 'lo', + iso639_3: 'lao', name: 'Lao', exampleText: 'ອ່ານ', }, { iso: 'ja', + iso639_3: 'jpn', name: 'Japanese', exampleText: '読め', isTextLookupWorthy: isStringPartiallyJapanese, @@ -165,11 +180,13 @@ const languageDescriptors = [ }, { iso: 'km', + iso639_3: 'khm', name: 'Khmer', exampleText: 'អាន', }, { iso: 'ko', + iso639_3: 'kor', name: 'Korean', exampleText: '읽어', textPreprocessors: { @@ -182,24 +199,28 @@ const languageDescriptors = [ }, { iso: 'nl', + iso639_3: 'nld', name: 'Dutch', exampleText: 'lezen', textPreprocessors: capitalizationPreprocessors, }, { iso: 'pl', + iso639_3: 'pol', name: 'Polish', exampleText: 'czytacie', textPreprocessors: capitalizationPreprocessors, }, { iso: 'pt', + iso639_3: 'por', name: 'Portuguese', exampleText: 'ler', textPreprocessors: capitalizationPreprocessors, }, { iso: 'ro', + iso639_3: 'ron', name: 'Romanian', exampleText: 'citit', textPreprocessors: { @@ -209,6 +230,7 @@ const languageDescriptors = [ }, { iso: 'ru', + iso639_3: 'rus', name: 'Russian', exampleText: 'читать', textPreprocessors: { @@ -219,6 +241,7 @@ const languageDescriptors = [ }, { iso: 'sga', + iso639_3: 'sga', name: 'Old Irish', exampleText: 'légaid', textPreprocessors: { @@ -229,6 +252,7 @@ const languageDescriptors = [ }, { iso: 'sh', + iso639_3: 'hbs', name: 'Serbo-Croatian', exampleText: 'čitaše', textPreprocessors: { @@ -238,6 +262,7 @@ const languageDescriptors = [ }, { iso: 'sq', + iso639_3: 'sqi', name: 'Albanian', exampleText: 'ndihmojme', textPreprocessors: capitalizationPreprocessors, @@ -245,23 +270,27 @@ const languageDescriptors = [ }, { iso: 'sv', + iso639_3: 'swe', name: 'Swedish', exampleText: 'läsa', textPreprocessors: capitalizationPreprocessors, }, { iso: 'th', + iso639_3: 'tha', name: 'Thai', exampleText: 'อ่าน', }, { iso: 'tr', + iso639_3: 'tur', name: 'Turkish', exampleText: 'okuyor', textPreprocessors: capitalizationPreprocessors, }, { iso: 'vi', + iso639_3: 'vie', name: 'Vietnamese', exampleText: 'đọc', textPreprocessors: { @@ -271,11 +300,13 @@ const languageDescriptors = [ }, { iso: 'yue', + iso639_3: 'yue', name: 'Cantonese', exampleText: '讀', }, { iso: 'zh', + iso639_3: 'zho', name: 'Chinese', exampleText: '读', isTextLookupWorthy: isStringPartiallyChinese, diff --git a/ext/js/language/languages.js b/ext/js/language/languages.js index 7759fda5..96f7a080 100755 --- a/ext/js/language/languages.js +++ b/ext/js/language/languages.js @@ -22,8 +22,8 @@ import {languageDescriptorMap} from './language-descriptors.js'; */ export function getLanguageSummaries() { const results = []; - for (const {name, iso, exampleText} of languageDescriptorMap.values()) { - results.push({name, iso, exampleText}); + for (const {name, iso, iso639_3, exampleText} of languageDescriptorMap.values()) { + results.push({name, iso, iso639_3, exampleText}); } return results; } diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 2d1bc4ec..d378d043 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -25,6 +25,16 @@ import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; import {SimpleDOMParser} from '../dom/simple-dom-parser.js'; import {isStringEntirelyKana} from '../language/ja/japanese.js'; +/** @type {RequestInit} */ +const DEFAULT_REQUEST_INIT_PARAMS = { + method: 'GET', + mode: 'cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer', +}; + export class AudioDownloader { /** * @param {RequestBuilder} requestBuilder @@ -39,6 +49,7 @@ export class AudioDownloader { ['jpod101', this._getInfoJpod101.bind(this)], ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)], ['jisho', this._getInfoJisho.bind(this)], + ['lingua-libre', this._getInfoLinguaLibre.bind(this)], ['text-to-speech', this._getInfoTextToSpeech.bind(this)], ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)], ['custom', this._getInfoCustom.bind(this)], @@ -50,13 +61,14 @@ export class AudioDownloader { * @param {import('audio').AudioSourceInfo} source * @param {string} term * @param {string} reading + * @param {import('language').LanguageSummary} languageSummary * @returns {Promise<import('audio-downloader').Info[]>} */ - async getTermAudioInfoList(source, term, reading) { + async getTermAudioInfoList(source, term, reading, languageSummary) { const handler = this._getInfoHandlers.get(source.type); if (typeof handler === 'function') { try { - return await handler(term, reading, source); + return await handler(term, reading, source, languageSummary); } catch (e) { // NOP } @@ -70,12 +82,13 @@ export class AudioDownloader { * @param {string} term * @param {string} reading * @param {?number} idleTimeout + * @param {import('language').LanguageSummary} languageSummary * @returns {Promise<import('audio-downloader').AudioBinaryBase64>} */ - async downloadTermAudio(sources, preferredAudioIndex, term, reading, idleTimeout) { + async downloadTermAudio(sources, preferredAudioIndex, term, reading, idleTimeout, languageSummary) { const errors = []; for (const source of sources) { - let infoList = await this.getTermAudioInfoList(source, term, reading); + let infoList = await this.getTermAudioInfoList(source, term, reading, languageSummary); if (typeof preferredAudioIndex === 'number') { infoList = (preferredAudioIndex >= 0 && preferredAudioIndex < infoList.length ? [infoList[preferredAudioIndex]] : []); } @@ -137,12 +150,8 @@ export class AudioDownloader { vulgar: 'true', }); const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { + ...DEFAULT_REQUEST_INIT_PARAMS, method: 'POST', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -181,14 +190,7 @@ export class AudioDownloader { /** @type {import('audio-downloader').GetInfoHandler} */ async _getInfoJisho(term, reading) { const fetchUrl = `https://jisho.org/search/${term}`; - const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', - }); + const response = await this._requestBuilder.fetchAnonymous(fetchUrl, DEFAULT_REQUEST_INIT_PARAMS); const responseText = await response.text(); const dom = this._createSimpleDOMParser(responseText); @@ -212,6 +214,44 @@ export class AudioDownloader { } /** @type {import('audio-downloader').GetInfoHandler} */ + async _getInfoLinguaLibre(term, _reading, _details, languageSummary) { + if (typeof languageSummary !== 'object' || languageSummary === null) { + throw new Error('Invalid arguments'); + } + const {iso639_3} = languageSummary; + const searchCategory = `incategory:"Lingua_Libre_pronunciation-${iso639_3}"`; + const searchString = `-${term}.wav`; + const fetchUrl = `https://commons.wikimedia.org/w/api.php?action=query&format=json&list=search&srsearch=intitle:/${searchString}/i+${searchCategory}&srnamespace=6&origin=*`; + + const response = await this._requestBuilder.fetchAnonymous(fetchUrl, DEFAULT_REQUEST_INIT_PARAMS); + + /** @type {import('audio-downloader').LinguaLibreLookupResponse} */ + const lookupResponse = await readResponseJson(response); + + const lookupResults = lookupResponse.query.search; + + const fetchFileInfos = lookupResults.map(async ({title}) => { + const fileInfoURL = `https://commons.wikimedia.org/w/api.php?action=query&format=json&titles=${title}&prop=imageinfo&iiprop=user|url&origin=*`; + const response2 = await this._requestBuilder.fetchAnonymous(fileInfoURL, DEFAULT_REQUEST_INIT_PARAMS); + /** @type {import('audio-downloader').LinguaLibreFileResponse} */ + const fileResponse = await readResponseJson(response2); + const fileResults = fileResponse.query.pages; + const results = []; + for (const page of Object.values(fileResults)) { + const fileUrl = page.imageinfo[0].url; + const fileUser = page.imageinfo[0].user; + const validFilenameTest = new RegExp(`^File:LL-Q\\d+\\s+\\(${iso639_3}\\)-(\\w+ \\()?${fileUser}\\)?-${term}\\.wav$`, 'i'); + if (validFilenameTest.test(title)) { + results.push({type: 'url', url: fileUrl, name: fileUser}); + } + } + return /** @type {import('audio-downloader').Info1[]} */ (results); + }); + + return (await Promise.all(fetchFileInfos)).flat(); + } + + /** @type {import('audio-downloader').GetInfoHandler} */ async _getInfoTextToSpeech(term, reading, details) { if (typeof details !== 'object' || details === null) { throw new Error('Invalid arguments'); @@ -259,14 +299,7 @@ export class AudioDownloader { } url = this._getCustomUrl(term, reading, url); - const response = await this._requestBuilder.fetchAnonymous(url, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', - }); + const response = await this._requestBuilder.fetchAnonymous(url, DEFAULT_REQUEST_INIT_PARAMS); if (!response.ok) { throw new Error(`Invalid response: ${response.status}`); @@ -345,12 +378,7 @@ export class AudioDownloader { } const response = await this._requestBuilder.fetchAnonymous(url, { - method: 'GET', - mode: 'cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', + ...DEFAULT_REQUEST_INIT_PARAMS, signal, }); @@ -429,12 +457,8 @@ export class AudioDownloader { async _getCustomAudioListSchema() { const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json'); const response = await fetch(url, { - method: 'GET', + ...DEFAULT_REQUEST_INIT_PARAMS, mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer', }); return await readResponseJson(response); } diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js index 34d7adaa..ac79b51a 100644 --- a/ext/js/pages/settings/audio-controller.js +++ b/ext/js/pages/settings/audio-controller.js @@ -221,6 +221,7 @@ export class AudioController extends EventDispatcher { 'jpod101', 'jpod101-alternate', 'jisho', + 'lingua-libre', 'custom', ]; for (const type of typesAvailable) { @@ -488,6 +489,7 @@ class AudioSourceEntry { case 'jpod101': case 'jpod101-alternate': case 'jisho': + case 'lingua-libre': case 'text-to-speech': case 'text-to-speech-reading': case 'custom': |