diff options
| author | Darius Jahandarie <djahandarie@gmail.com> | 2023-12-06 03:53:16 +0000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-06 03:53:16 +0000 | 
| commit | bd5bc1a5db29903bc098995cd9262c4576bf76af (patch) | |
| tree | c9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/media/audio-downloader.js | |
| parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
| parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) | |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/media/audio-downloader.js')
| -rw-r--r-- | ext/js/media/audio-downloader.js | 126 | 
1 files changed, 109 insertions, 17 deletions
| diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 1720a5d9..e041cc67 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -17,17 +17,25 @@   */  import {RequestBuilder} from '../background/request-builder.js'; +import {ExtensionError} from '../core/extension-error.js';  import {JsonSchema} from '../data/json-schema.js';  import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';  import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js';  import {SimpleDOMParser} from '../dom/simple-dom-parser.js';  export class AudioDownloader { +    /** +     * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil, requestBuilder: RequestBuilder}} details +     */      constructor({japaneseUtil, requestBuilder}) { +        /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */          this._japaneseUtil = japaneseUtil; +        /** @type {RequestBuilder} */          this._requestBuilder = requestBuilder; +        /** @type {?JsonSchema} */          this._customAudioListSchema = null; -        this._getInfoHandlers = new Map([ +        /** @type {Map<import('settings').AudioSourceType, import('audio-downloader').GetInfoHandler>} */ +        this._getInfoHandlers = new Map(/** @type {[name: import('settings').AudioSourceType, handler: import('audio-downloader').GetInfoHandler][]} */ ([              ['jpod101', this._getInfoJpod101.bind(this)],              ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)],              ['jisho', this._getInfoJisho.bind(this)], @@ -35,9 +43,15 @@ export class AudioDownloader {              ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)],              ['custom', this._getInfoCustom.bind(this)],              ['custom-json', this._getInfoCustomJson.bind(this)] -        ]); +        ]));      } +    /** +     * @param {import('audio').AudioSourceInfo} source +     * @param {string} term +     * @param {string} reading +     * @returns {Promise<import('audio-downloader').Info[]>} +     */      async getTermAudioInfoList(source, term, reading) {          const handler = this._getInfoHandlers.get(source.type);          if (typeof handler === 'function') { @@ -50,6 +64,14 @@ export class AudioDownloader {          return [];      } +    /** +     * @param {import('audio').AudioSourceInfo[]} sources +     * @param {?number} preferredAudioIndex +     * @param {string} term +     * @param {string} reading +     * @param {?number} idleTimeout +     * @returns {Promise<import('audio-downloader').AudioBinaryBase64>} +     */      async downloadTermAudio(sources, preferredAudioIndex, term, reading, idleTimeout) {          const errors = [];          for (const source of sources) { @@ -70,28 +92,34 @@ export class AudioDownloader {              }          } -        const error = new Error('Could not download audio'); +        const error = new ExtensionError('Could not download audio');          error.data = {errors};          throw error;      }      // Private +    /** +     * @param {string} url +     * @param {string} base +     * @returns {string} +     */      _normalizeUrl(url, base) {          return new URL(url, base).href;      } +    /** @type {import('audio-downloader').GetInfoHandler} */      async _getInfoJpod101(term, reading) {          if (reading === term && this._japaneseUtil.isStringEntirelyKana(term)) {              reading = term; -            term = null; +            term = '';          }          const params = new URLSearchParams(); -        if (term) { +        if (term.length > 0) {              params.set('kanji', term);          } -        if (reading) { +        if (reading.length > 0) {              params.set('kana', reading);          } @@ -99,6 +127,7 @@ export class AudioDownloader {          return [{type: 'url', url}];      } +    /** @type {import('audio-downloader').GetInfoHandler} */      async _getInfoJpod101Alternate(term, reading) {          const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post';          const data = new URLSearchParams({ @@ -149,6 +178,7 @@ export class AudioDownloader {          throw new Error('Failed to find audio URL');      } +    /** @type {import('audio-downloader').GetInfoHandler} */      async _getInfoJisho(term, reading) {          const fetchUrl = `https://jisho.org/search/${term}`;          const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { @@ -181,26 +211,52 @@ export class AudioDownloader {          throw new Error('Failed to find audio URL');      } -    async _getInfoTextToSpeech(term, reading, {voice}) { -        if (!voice) { -            throw new Error('No voice'); +    /** @type {import('audio-downloader').GetInfoHandler} */ +    async _getInfoTextToSpeech(term, reading, details) { +        if (typeof details !== 'object' || details === null) { +            throw new Error('Invalid arguments'); +        } +        const {voice} = details; +        if (typeof voice !== 'string') { +            throw new Error('Invalid voice');          }          return [{type: 'tts', text: term, voice: voice}];      } -    async _getInfoTextToSpeechReading(term, reading, {voice}) { -        if (!voice) { -            throw new Error('No voice'); +    /** @type {import('audio-downloader').GetInfoHandler} */ +    async _getInfoTextToSpeechReading(term, reading, details) { +        if (typeof details !== 'object' || details === null) { +            throw new Error('Invalid arguments'); +        } +        const {voice} = details; +        if (typeof voice !== 'string') { +            throw new Error('Invalid voice');          }          return [{type: 'tts', text: reading, voice: voice}];      } -    async _getInfoCustom(term, reading, {url}) { +    /** @type {import('audio-downloader').GetInfoHandler} */ +    async _getInfoCustom(term, reading, details) { +        if (typeof details !== 'object' || details === null) { +            throw new Error('Invalid arguments'); +        } +        let {url} = details; +        if (typeof url !== 'string') { +            throw new Error('Invalid url'); +        }          url = this._getCustomUrl(term, reading, url);          return [{type: 'url', url}];      } -    async _getInfoCustomJson(term, reading, {url}) { +    /** @type {import('audio-downloader').GetInfoHandler} */ +    async _getInfoCustomJson(term, reading, details) { +        if (typeof details !== 'object' || details === null) { +            throw new Error('Invalid arguments'); +        } +        let {url} = details; +        if (typeof url !== 'string') { +            throw new Error('Invalid url'); +        }          url = this._getCustomUrl(term, reading, url);          const response = await this._requestBuilder.fetchAnonymous(url, { @@ -220,12 +276,14 @@ export class AudioDownloader {          if (this._customAudioListSchema === null) {              const schema = await this._getCustomAudioListSchema(); -            this._customAudioListSchema = new JsonSchema(schema); +            this._customAudioListSchema = new JsonSchema(/** @type {import('json-schema').Schema} */ (schema));          }          this._customAudioListSchema.validate(responseJson); +        /** @type {import('audio-downloader').Info[]} */          const results = [];          for (const {url: url2, name} of responseJson.audioSources) { +            /** @type {import('audio-downloader').Info1} */              const info = {type: 'url', url: url2};              if (typeof name === 'string') { info.name = name; }              results.push(info); @@ -233,17 +291,32 @@ export class AudioDownloader {          return results;      } +    /** +     * @param {string} term +     * @param {string} reading +     * @param {string} url +     * @returns {string} +     * @throws {Error} +     */      _getCustomUrl(term, reading, url) {          if (typeof url !== 'string') {              throw new Error('No custom URL defined');          }          const data = {term, reading}; -        return url.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0)); +        return url.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[/** @type {'term'|'reading'} */ (m1)]}` : m0));      } +    /** +     * @param {string} url +     * @param {import('settings').AudioSourceType} sourceType +     * @param {?number} idleTimeout +     * @returns {Promise<import('audio-downloader').AudioBinaryBase64>} +     */      async _downloadAudioFromUrl(url, sourceType, idleTimeout) {          let signal; +        /** @type {?import('request-builder.js').ProgressCallback} */          let onProgress = null; +        /** @type {?import('core').Timeout} */          let idleTimer = null;          if (typeof idleTimeout === 'number') {              const abortController = new AbortController(); @@ -252,7 +325,9 @@ export class AudioDownloader {                  abortController.abort('Idle timeout');              };              onProgress = (done) => { -                clearTimeout(idleTimer); +                if (idleTimer !== null) { +                    clearTimeout(idleTimer); +                }                  idleTimer = done ? null : setTimeout(onIdleTimeout, idleTimeout);              };              idleTimer = setTimeout(onIdleTimeout, idleTimeout); @@ -287,6 +362,11 @@ export class AudioDownloader {          return {data, contentType};      } +    /** +     * @param {ArrayBuffer} arrayBuffer +     * @param {import('settings').AudioSourceType} sourceType +     * @returns {Promise<boolean>} +     */      async _isAudioBinaryValid(arrayBuffer, sourceType) {          switch (sourceType) {              case 'jpod101': @@ -304,6 +384,10 @@ export class AudioDownloader {          }      } +    /** +     * @param {ArrayBuffer} arrayBuffer +     * @returns {Promise<string>} +     */      async _arrayBufferDigest(arrayBuffer) {          const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer)));          let digest = ''; @@ -313,6 +397,11 @@ export class AudioDownloader {          return digest;      } +    /** +     * @param {string} content +     * @returns {import('simple-dom-parser').ISimpleDomParser} +     * @throws {Error} +     */      _createSimpleDOMParser(content) {          if (typeof NativeSimpleDOMParser !== 'undefined' && NativeSimpleDOMParser.isSupported()) {              return new NativeSimpleDOMParser(content); @@ -323,6 +412,9 @@ export class AudioDownloader {          }      } +    /** +     * @returns {Promise<unknown>} +     */      async _getCustomAudioListSchema() {          const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json');          const response = await fetch(url, { |