diff options
| -rw-r--r-- | ext/bg/data/custom-audio-list-schema.json | 33 | ||||
| -rw-r--r-- | ext/bg/data/options-schema.json | 6 | ||||
| -rw-r--r-- | ext/bg/js/audio-downloader.js | 62 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 3 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 1 | ||||
| -rw-r--r-- | ext/bg/settings2.html | 32 | ||||
| -rw-r--r-- | ext/mixed/css/material.css | 1 | ||||
| -rw-r--r-- | ext/mixed/js/display-audio.js | 8 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 4 | ||||
| -rw-r--r-- | test/test-options-util.js | 1 | 
10 files changed, 139 insertions, 12 deletions
| diff --git a/ext/bg/data/custom-audio-list-schema.json b/ext/bg/data/custom-audio-list-schema.json new file mode 100644 index 00000000..2cb3ca78 --- /dev/null +++ b/ext/bg/data/custom-audio-list-schema.json @@ -0,0 +1,33 @@ +{ +    "$schema": "http://json-schema.org/draft-07/schema#", +    "type": "object", +    "required": [ +        "type", +        "audioSources" +    ], +    "additionalProperties": false, +    "properties": { +        "type": { +            "type": "string", +            "const": "audioSourceList" +        }, +        "audioSources": { +            "type": "array", +            "items": { +                "type": "object", +                "required": [ +                    "url" +                ], +                "additionalProperties": false, +                "properties": { +                    "name": { +                        "type": "string" +                    }, +                    "url": { +                        "type": "string" +                    } +                } +            } +        } +    } +} diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 189c8621..dfc553ed 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -348,6 +348,7 @@                                      "volume",                                      "autoPlay",                                      "customSourceUrl", +                                    "customSourceType",                                      "textToSpeechVoice"                                  ],                                  "properties": { @@ -387,6 +388,11 @@                                          "type": "string",                                          "default": ""                                      }, +                                    "customSourceType": { +                                        "type": "string", +                                        "enum": ["audio", "json"], +                                        "default": "audio" +                                    },                                      "textToSpeechVoice": {                                          "type": "string",                                          "default": "" diff --git a/ext/bg/js/audio-downloader.js b/ext/bg/js/audio-downloader.js index 495b6399..62abda8f 100644 --- a/ext/bg/js/audio-downloader.js +++ b/ext/bg/js/audio-downloader.js @@ -16,6 +16,7 @@   */  /* global + * JsonSchemaValidator   * NativeSimpleDOMParser   * SimpleDOMParser   */ @@ -24,6 +25,8 @@ class AudioDownloader {      constructor({japaneseUtil, requestBuilder}) {          this._japaneseUtil = japaneseUtil;          this._requestBuilder = requestBuilder; +        this._customAudioListSchema = null; +        this._schemaValidator = null;          this._getInfoHandlers = new Map([              ['jpod101', this._getInfoJpod101.bind(this)],              ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)], @@ -183,13 +186,50 @@ class AudioDownloader {          return [{type: 'tts', text: reading || expression, voice: textToSpeechVoice}];      } -    async _getInfoCustom(expression, reading, {customSourceUrl}) { +    async _getInfoCustom(expression, reading, {customSourceUrl, customSourceType}) {          if (typeof customSourceUrl !== 'string') {              throw new Error('No custom URL defined');          }          const data = {expression, reading};          const url = customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0)); -        return [{type: 'url', url}]; + +        switch (customSourceType) { +            case 'json': +                return await this._getInfoCustomJson(url); +            default: +                return [{type: 'url', url}]; +        } +    } + +    async _getInfoCustomJson(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 responseJson = await response.json(); + +        const schema = await this._getCustomAudioListSchema(); +        if (this._schemaValidator === null) { +            this._schemaValidator = new JsonSchemaValidator(); +        } +        this._schemaValidator.validate(responseJson, schema); + +        const results = []; +        for (const {url: url2, name} of responseJson.audioSources) { +            const info = {type: 'url', url: url2}; +            if (typeof name === 'string') { info.name = name; } +            results.push(info); +        } +        return results;      }      async _downloadAudioFromUrl(url, source) { @@ -254,4 +294,22 @@ class AudioDownloader {              throw new Error('DOM parsing not supported');          }      } + +    async _getCustomAudioListSchema() { +        let schema = this._customAudioListSchema; +        if (schema === null) { +            const url = chrome.runtime.getURL('/bg/data/custom-audio-list-schema.json'); +            const response = await fetch(url, { +                method: 'GET', +                mode: 'no-cors', +                cache: 'default', +                credentials: 'omit', +                redirect: 'follow', +                referrerPolicy: 'no-referrer' +            }); +            schema = await response.json(); +            this._customAudioListSchema = schema; +        } +        return schema; +    }  } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 2949cbed..60739aab 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -1581,7 +1581,7 @@ class Backend {              throw new Error('Invalid reading and expression');          } -        const {sources, customSourceUrl} = details; +        const {sources, customSourceUrl, customSourceType} = details;          const data = await this._downloadDefinitionAudio(              sources,              expression, @@ -1589,6 +1589,7 @@ class Backend {              {                  textToSpeechVoice: null,                  customSourceUrl, +                customSourceType,                  binary: true,                  disableCache: true              } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 1690efb0..10919ae3 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -726,6 +726,7 @@ class OptionsUtil {                  windowType: 'popup',                  windowState: 'normal'              }; +            profile.options.audio.customSourceType = 'audio';          }          return options;      } diff --git a/ext/bg/settings2.html b/ext/bg/settings2.html index 62598352..448cc67d 100644 --- a/ext/bg/settings2.html +++ b/ext/bg/settings2.html @@ -2205,15 +2205,41 @@                      </div>                  </div>                  <div class="settings-item-right"> -                    <input type="text" spellcheck="false" autocomplete="off" data-setting="audio.customSourceUrl" placeholder="None"> +                    <div class="settings-item-group"> +                        <div class="settings-item-group-item"> +                            <div class="settings-item-group-item-label">Type</div> +                            <select class="short-width short-height" data-setting="audio.customSourceType"> +                                <option value="audio">Audio</option> +                                <option value="json">JSON</option> +                            </select> +                        </div> +                        <div class="settings-item-group-item"> +                            <div class="settings-item-group-item-label">URL</div> +                            <input class="short-height" type="text" spellcheck="false" autocomplete="off" data-setting="audio.customSourceUrl" placeholder="None"> +                        </div> +                    </div>                  </div>              </div>              <div class="settings-item-children more" hidden>                  <p> -                    URL format used for fetching audio clips in <em>Custom</em> mode. +                    The <em>URL</em> property specifies the URL format used for fetching audio clips in <em>Custom</em> mode.                      The replacement tags <code data-select-on-click="">{expression}</code> and <code data-select-on-click="">{reading}</code> can be used to specify which                      expression and reading is being looked up.<br> -                    Example: <a data-select-on-click="">http://localhost/audio.mp3?expression={expression}&reading={reading}</a> +                </p> +                <p> +                    The <em>Type</em> property specifies how the URL is handled when looking up audio: +                </p> +                <ul> +                    <li> +                        <strong>Audio</strong> - The link is treated as a direct link to an audio file that the browser can play. +                    </li> +                    <li> +                        <strong>JSON</strong> - The link is interpreted as a link to a JSON file, which is downloaded and parsed for audio URLs. +                        The format of the JSON file is specified in <a href="/bg/data/custom-audio-list-schema.json" target="_blank" rel="noopener noreferrer">this schema file</a>. +                    </li> +                </ul> +                <p> +                    Example URL: <a data-select-on-click="">http://localhost/audio.mp3?expression={expression}&reading={reading}</a>                  </p>                  <p>                      <a class="more-toggle" data-parent-distance="3">Less…</a> diff --git a/ext/mixed/css/material.css b/ext/mixed/css/material.css index ec55120b..d144937b 100644 --- a/ext/mixed/css/material.css +++ b/ext/mixed/css/material.css @@ -970,6 +970,7 @@ button.popup-menu-item:disabled {      height: calc(16em / 14);      background-color: var(--text-color);      margin-right: 0.5em; +    flex: 0 0 auto;  }  .popup-menu-item-icon:not([hidden]) {      display: block; diff --git a/ext/mixed/js/display-audio.js b/ext/mixed/js/display-audio.js index c60831b1..f624d85b 100644 --- a/ext/mixed/js/display-audio.js +++ b/ext/mixed/js/display-audio.js @@ -112,7 +112,7 @@ class DisplayAudio {          const {expression, reading} = expressionReading;          const audioOptions = this._getAudioOptions(); -        const {textToSpeechVoice, customSourceUrl, volume} = audioOptions; +        const {textToSpeechVoice, customSourceUrl, customSourceType, volume} = audioOptions;          if (!Array.isArray(sources)) {              ({sources} = audioOptions);          } @@ -126,7 +126,7 @@ class DisplayAudio {              // Create audio              let audio;              let title; -            const info = await this._createExpressionAudio(sources, sourceDetailsMap, expression, reading, {textToSpeechVoice, customSourceUrl}); +            const info = await this._createExpressionAudio(sources, sourceDetailsMap, expression, reading, {textToSpeechVoice, customSourceUrl, customSourceType});              if (info !== null) {                  let source;                  ({audio, source} = info); @@ -520,13 +520,13 @@ class DisplayAudio {              // Entry info              entry.index = i; -            const {audio, audioResolved, title} = infoList[i]; +            const {audio, audioResolved, info: {name}} = infoList[i];              if (audioResolved) { entry.valid = (audio !== null); }              const labelNode = entry.node.querySelector('.popup-menu-item-label');              let label = defaultLabel;              if (ii > 1) { label = `${label} ${i + 1}`; } -            if (typeof title === 'string' && title.length > 0) { label += `: ${title}`; } +            if (typeof name === 'string' && name.length > 0) { label += `: ${name}`; }              labelNode.textContent = label;          } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index eb8b2900..6c97cb84 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -1432,13 +1432,13 @@ class Display extends EventDispatcher {      async _injectAnkiNoteMedia(definition, mode, options, fields) {          const {              anki: {screenshot: {format, quality}}, -            audio: {sources, customSourceUrl} +            audio: {sources, customSourceUrl, customSourceType}          } = options;          const timestamp = Date.now();          const ownerFrameId = this._ownerFrameId;          const definitionDetails = this._getDefinitionDetailsForNote(definition); -        const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl} : null); +        const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl, customSourceType} : null);          const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {ownerFrameId, format, quality} : null);          const clipboardDetails = {              image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), diff --git a/test/test-options-util.js b/test/test-options-util.js index 5597dbba..0ec40a9b 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -304,6 +304,7 @@ function createProfileOptionsUpdatedTestData1() {              volume: 100,              autoPlay: false,              customSourceUrl: '', +            customSourceType: 'audio',              textToSpeechVoice: ''          },          scanning: { |