summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/data/custom-audio-list-schema.json33
-rw-r--r--ext/bg/data/options-schema.json6
-rw-r--r--ext/bg/js/audio-downloader.js62
-rw-r--r--ext/bg/js/backend.js3
-rw-r--r--ext/bg/js/options.js1
-rw-r--r--ext/bg/settings2.html32
-rw-r--r--ext/mixed/css/material.css1
-rw-r--r--ext/mixed/js/display-audio.js8
-rw-r--r--ext/mixed/js/display.js4
9 files changed, 138 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&hellip;</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'),